diff options
Diffstat (limited to 'e-util')
478 files changed, 169300 insertions, 283 deletions
diff --git a/e-util/Makefile.am b/e-util/Makefile.am index 4464f93045..15a9bcd8ac 100644 --- a/e-util/Makefile.am +++ b/e-util/Makefile.am @@ -1,3 +1,5 @@ +NULL = + eutilincludedir = $(privincludedir)/e-util ecpsdir = $(privdatadir)/ecps ruledir = $(privdatadir) @@ -22,133 +24,650 @@ e-marshal.c: e-marshal.list ENUM_GENERATED = e-util-enumtypes.h e-util-enumtypes.c MARSHAL_GENERATED = e-marshal.c e-marshal.h -if OS_WIN32 -PLATFORM_SOURCES = e-win32-reloc.c e-win32-defaults.c e-win32-defaults.h -endif +error_DATA = \ + e-system.error \ + filter.error \ + widgets.error \ + $(NULL) +errordir = $(privdatadir)/errors +@EVO_PLUGIN_RULE@ + +ui_DATA = \ + e-send-options.ui \ + e-table-config.ui \ + e-timezone-dialog.ui \ + filter.ui \ + gal-define-views.ui \ + gal-view-instance-save-as-dialog.ui \ + gal-view-new-dialog.ui \ + $(NULL) + +xpm_icons = \ + arrow-down.xpm \ + arrow-up.xpm \ + check-empty.xpm \ + check-filled.xpm \ + tree-expanded.xpm \ + tree-unexpanded.xpm \ + $(NULL) privsolib_LTLIBRARIES = libeutil.la -eutilinclude_HEADERS = \ - e-activity.h \ - e-bit-array.h \ - e-categories-config.h \ - e-charset.h \ - e-config.h \ - e-datetime-format.h \ - e-dialog-utils.h \ - e-dialog-widgets.h \ - e-event.h \ - e-file-request.h \ - e-file-utils.h \ - e-html-utils.h \ - e-icon-factory.h \ - e-import.h \ - e-marshal.h \ - e-mktemp.h \ - e-poolv.h \ - e-print.h \ - e-plugin.h \ - e-plugin-ui.h \ - e-selection.h \ - e-sorter.h \ - e-sorter-array.h \ - e-source-util.h \ - e-stock-request.h \ - e-text-event-processor-emacs-like.h \ - e-text-event-processor-types.h \ - e-text-event-processor.h \ - e-ui-manager.h \ - e-util.h \ - e-util-enums.h \ - e-util-enumtypes.h \ - e-unicode.h - -libeutil_la_CPPFLAGS = \ - $(AM_CPPFLAGS) \ - -I$(top_srcdir) \ - -I$(top_builddir) \ - -I$(top_srcdir)/widgets \ - -DEVOLUTION_BINDIR=\""$(bindir)"\" \ - -DEVOLUTION_DATADIR=\""$(datadir)"\" \ - -DEVOLUTION_ECPSDIR=\""$(ecpsdir)"\" \ - -DEVOLUTION_ETSPECDIR=\""$(etspecdir)"\" \ - -DEVOLUTION_GALVIEWSDIR=\""$(viewsdir)"\" \ - -DEVOLUTION_HELPDIR=\""$(evolutionhelpdir)"\" \ - -DEVOLUTION_ICONDIR=\""$(icondir)"\" \ - -DEVOLUTION_IMAGESDIR=\""$(imagesdir)"\" \ - -DEVOLUTION_LIBDIR=\""$(datadir)"\" \ - -DEVOLUTION_LIBEXECDIR=\""$(privlibexecdir)"\" \ - -DEVOLUTION_LOCALEDIR=\""$(localedir)"\" \ - -DEVOLUTION_MODULEDIR=\""$(moduledir)"\" \ - -DEVOLUTION_PLUGINDIR=\""$(plugindir)"\" \ - -DEVOLUTION_PREFIX=\""$(prefix)"\" \ - -DEVOLUTION_PRIVDATADIR=\""$(privdatadir)"\" \ - -DEVOLUTION_SOUNDDIR=\""$(soundsdir)"\" \ - -DEVOLUTION_SYSCONFDIR=\""$(sysconfdir)"\" \ - -DEVOLUTION_TOOLSDIR=\""$(privlibexecdir)"\" \ - -DEVOLUTION_UIDIR=\""$(uidir)"\" \ - -DEVOLUTION_RULEDIR=\"$(ruledir)\" \ - -DG_LOG_DOMAIN=\"e-utils\" \ - $(EVOLUTION_DATA_SERVER_CFLAGS) \ - $(GNOME_PLATFORM_CFLAGS) - -libeutil_la_SOURCES = \ - $(eutilinclude_HEADERS) \ - e-activity.c \ - e-bit-array.c \ - e-categories-config.c \ - e-charset.c \ - e-config.c \ - e-datetime-format.c \ - e-dialog-utils.c \ - e-dialog-widgets.c \ - e-event.c \ - e-file-request.c \ - e-file-utils.c \ - e-html-utils.c \ - e-icon-factory.c \ - e-import.c \ - e-marshal.c \ - e-mktemp.c \ - e-poolv.c \ - e-plugin.c \ - e-plugin-ui.c \ - e-print.c \ - e-selection.c \ - e-sorter.c \ - e-sorter-array.c \ - e-source-util.c \ - e-stock-request.c \ - e-text-event-processor-emacs-like.c \ - e-text-event-processor.c \ - e-ui-manager.c \ - e-util.c \ - e-unicode.c \ - e-util-enumtypes.c \ - e-util-private.h \ - $(PLATFORM_SOURCES) +noinst_PROGRAMS = \ + evolution-source-viewer \ + test-calendar \ + test-category-completion \ + test-contact-store \ + test-dateedit \ + test-mail-signatures \ + test-name-selector \ + test-preferences-window \ + test-source-combo-box \ + test-source-config \ + test-source-selector \ + $(NULL) + +libeutil_la_CPPFLAGS = \ + $(AM_CPPFLAGS) \ + -I$(top_srcdir) \ + -I$(top_builddir) \ + -DLIBEUTIL_COMPILATION \ + -DEVOLUTION_BINDIR=\""$(bindir)"\" \ + -DEVOLUTION_DATADIR=\""$(datadir)"\" \ + -DEVOLUTION_ECPSDIR=\""$(ecpsdir)"\" \ + -DEVOLUTION_ETSPECDIR=\""$(etspecdir)"\" \ + -DEVOLUTION_GALVIEWSDIR=\""$(viewsdir)"\" \ + -DEVOLUTION_HELPDIR=\""$(evolutionhelpdir)"\" \ + -DEVOLUTION_ICONDIR=\""$(icondir)"\" \ + -DEVOLUTION_IMAGESDIR=\""$(imagesdir)"\" \ + -DEVOLUTION_LIBDIR=\""$(datadir)"\" \ + -DEVOLUTION_LIBEXECDIR=\""$(privlibexecdir)"\" \ + -DEVOLUTION_LOCALEDIR=\""$(localedir)"\" \ + -DEVOLUTION_MODULEDIR=\""$(moduledir)"\" \ + -DEVOLUTION_PLUGINDIR=\""$(plugindir)"\" \ + -DEVOLUTION_PREFIX=\""$(prefix)"\" \ + -DEVOLUTION_PRIVDATADIR=\""$(privdatadir)"\" \ + -DEVOLUTION_SOUNDDIR=\""$(soundsdir)"\" \ + -DEVOLUTION_SYSCONFDIR=\""$(sysconfdir)"\" \ + -DEVOLUTION_TOOLSDIR=\""$(privlibexecdir)"\" \ + -DEVOLUTION_UIDIR=\""$(uidir)"\" \ + -DEVOLUTION_RULEDIR=\"$(ruledir)\" \ + -DG_LOG_DOMAIN=\"libeutil\" \ + $(EVOLUTION_DATA_SERVER_CFLAGS) \ + $(GNOME_PLATFORM_CFLAGS) \ + $(CHAMPLAIN_CFLAGS) \ + $(GEO_CFLAGS) \ + $(GTKHTML_CFLAGS) \ + $(NULL) + +eutilinclude_HEADERS = \ + e-util.h \ + e-action-combo-box.h \ + e-activity-bar.h \ + e-activity-proxy.h \ + e-activity.h \ + e-alarm-selector.h \ + e-alert-bar.h \ + e-alert-dialog.h \ + e-alert-sink.h \ + e-alert.h \ + e-attachment-bar.h \ + e-attachment-button.h \ + e-attachment-dialog.h \ + e-attachment-handler-image.h \ + e-attachment-handler-sendto.h \ + e-attachment-handler.h \ + e-attachment-icon-view.h \ + e-attachment-paned.h \ + e-attachment-store.h \ + e-attachment-tree-view.h \ + e-attachment-view.h \ + e-attachment.h \ + e-auth-combo-box.h \ + e-autocomplete-selector.h \ + e-bit-array.h \ + e-book-source-config.h \ + e-buffer-tagger.h \ + e-cal-source-config.h \ + e-calendar-item.h \ + e-calendar.h \ + e-canvas-background.h \ + e-canvas-utils.h \ + e-canvas-vbox.h \ + e-canvas.h \ + e-categories-config.h \ + e-categories-dialog.h \ + e-categories-editor.h \ + e-categories-selector.h \ + e-category-completion.h \ + e-category-editor.h \ + e-cell-checkbox.h \ + e-cell-combo.h \ + e-cell-date-edit.h \ + e-cell-date.h \ + e-cell-hbox.h \ + e-cell-number.h \ + e-cell-percent.h \ + e-cell-pixbuf.h \ + e-cell-popup.h \ + e-cell-renderer-color.h \ + e-cell-size.h \ + e-cell-text.h \ + e-cell-toggle.h \ + e-cell-tree.h \ + e-cell-vbox.h \ + e-cell.h \ + e-charset-combo-box.h \ + e-charset.h \ + e-client-utils.h \ + e-config.h \ + e-contact-map-window.h \ + e-contact-map.h \ + e-contact-marker.h \ + e-contact-store.h \ + e-dateedit.h \ + e-datetime-format.h \ + e-destination-store.h \ + e-dialog-utils.h \ + e-dialog-widgets.h \ + e-event.h \ + e-file-request.h \ + e-file-utils.h \ + e-filter-code.h \ + e-filter-color.h \ + e-filter-datespec.h \ + e-filter-element.h \ + e-filter-file.h \ + e-filter-input.h \ + e-filter-int.h \ + e-filter-option.h \ + e-filter-part.h \ + e-filter-rule.h \ + e-focus-tracker.h \ + e-html-utils.h \ + e-icon-factory.h \ + e-image-chooser.h \ + e-import-assistant.h \ + e-import.h \ + e-interval-chooser.h \ + e-mail-identity-combo-box.h \ + e-mail-signature-combo-box.h \ + e-mail-signature-editor.h \ + e-mail-signature-manager.h \ + e-mail-signature-preview.h \ + e-mail-signature-script-dialog.h \ + e-mail-signature-tree-view.h \ + e-map.h \ + e-marshal.h \ + e-menu-tool-action.h \ + e-menu-tool-button.h \ + e-misc-utils.h \ + e-mktemp.h \ + e-name-selector-dialog.h \ + e-name-selector-entry.h \ + e-name-selector-list.h \ + e-name-selector-model.h \ + e-name-selector.h \ + e-online-button.h \ + e-paned.h \ + e-passwords.h \ + e-picture-gallery.h \ + e-plugin-ui.h \ + e-plugin.h \ + e-poolv.h \ + e-popup-action.h \ + e-popup-menu.h \ + e-port-entry.h \ + e-preferences-window.h \ + e-preview-pane.h \ + e-print.h \ + e-printable.h \ + e-reflow-model.h \ + e-reflow.h \ + e-rule-context.h \ + e-rule-editor.h \ + e-search-bar.h \ + e-selectable.h \ + e-selection-model-array.h \ + e-selection-model-simple.h \ + e-selection-model.h \ + e-selection.h \ + e-send-options.h \ + e-sorter-array.h \ + e-sorter.h \ + e-source-combo-box.h \ + e-source-config-backend.h \ + e-source-config-dialog.h \ + e-source-config.h \ + e-source-selector-dialog.h \ + e-source-selector.h \ + e-source-util.h \ + e-spell-entry.h \ + e-stock-request.h \ + e-table-click-to-add.h \ + e-table-col-dnd.h \ + e-table-col.h \ + e-table-column-specification.h \ + e-table-config.h \ + e-table-defines.h \ + e-table-extras.h \ + e-table-field-chooser-dialog.h \ + e-table-field-chooser-item.h \ + e-table-field-chooser.h \ + e-table-group-container.h \ + e-table-group-leaf.h \ + e-table-group.h \ + e-table-header-item.h \ + e-table-header-utils.h \ + e-table-header.h \ + e-table-item.h \ + e-table-memory-callbacks.h \ + e-table-memory-store.h \ + e-table-memory.h \ + e-table-model.h \ + e-table-one.h \ + e-table-search.h \ + e-table-selection-model.h \ + e-table-sort-info.h \ + e-table-sorted-variable.h \ + e-table-sorted.h \ + e-table-sorter.h \ + e-table-sorting-utils.h \ + e-table-specification.h \ + e-table-state.h \ + e-table-subset-variable.h \ + e-table-subset.h \ + e-table-utils.h \ + e-table-without.h \ + e-table.h \ + e-text-event-processor-emacs-like.h \ + e-text-event-processor-types.h \ + e-text-event-processor.h \ + e-text-model-repos.h \ + e-text-model.h \ + e-text.h \ + e-timezone-dialog.h \ + e-tree-memory-callbacks.h \ + e-tree-memory.h \ + e-tree-model-generator.h \ + e-tree-model.h \ + e-tree-selection-model.h \ + e-tree-sorted.h \ + e-tree-table-adapter.h \ + e-tree.h \ + e-ui-manager.h \ + e-unicode.h \ + e-url-entry.h \ + e-util-enums.h \ + e-util-enumtypes.h \ + e-web-view-gtkhtml.h \ + e-web-view-preview.h \ + e-web-view.h \ + e-xml-utils.h \ + ea-calendar-cell.h \ + ea-calendar-item.h \ + ea-cell-table.h \ + ea-factory.h \ + ea-widgets.h \ + gal-a11y-e-cell-popup.h \ + gal-a11y-e-cell-registry.h \ + gal-a11y-e-cell-toggle.h \ + gal-a11y-e-cell-tree.h \ + gal-a11y-e-cell-vbox.h \ + gal-a11y-e-cell.h \ + gal-a11y-e-table-click-to-add-factory.h \ + gal-a11y-e-table-click-to-add.h \ + gal-a11y-e-table-column-header.h \ + gal-a11y-e-table-factory.h \ + gal-a11y-e-table-item-factory.h \ + gal-a11y-e-table-item.h \ + gal-a11y-e-table.h \ + gal-a11y-e-text-factory.h \ + gal-a11y-e-text.h \ + gal-a11y-e-tree-factory.h \ + gal-a11y-e-tree.h \ + gal-a11y-factory.h \ + gal-a11y-util.h \ + gal-define-views-dialog.h \ + gal-define-views-model.h \ + gal-view-collection.h \ + gal-view-etable.h \ + gal-view-factory-etable.h \ + gal-view-factory.h \ + gal-view-instance-save-as-dialog.h \ + gal-view-instance.h \ + gal-view-new-dialog.h \ + gal-view.h \ + $(NULL) + +if OS_WIN32 +PLATFORM_SOURCES = \ + e-win32-reloc.c \ + e-win32-defaults.c \ + e-win32-defaults.h \ + $(NULL) +endif + +libeutil_la_SOURCES = \ + $(eutilinclude_HEADERS) \ + e-action-combo-box.c \ + e-activity-bar.c \ + e-activity-proxy.c \ + e-activity.c \ + e-alarm-selector.c \ + e-alert-bar.c \ + e-alert-dialog.c \ + e-alert-sink.c \ + e-alert.c \ + e-attachment-bar.c \ + e-attachment-button.c \ + e-attachment-dialog.c \ + e-attachment-handler-image.c \ + e-attachment-handler-sendto.c \ + e-attachment-handler.c \ + e-attachment-icon-view.c \ + e-attachment-paned.c \ + e-attachment-store.c \ + e-attachment-tree-view.c \ + e-attachment-view.c \ + e-attachment.c \ + e-auth-combo-box.c \ + e-autocomplete-selector.c \ + e-bit-array.c \ + e-book-source-config.c \ + e-buffer-tagger.c \ + e-cal-source-config.c \ + e-calendar-item.c \ + e-calendar.c \ + e-canvas-background.c \ + e-canvas-utils.c \ + e-canvas-vbox.c \ + e-canvas.c \ + e-categories-config.c \ + e-categories-dialog.c \ + e-categories-editor.c \ + e-categories-selector.c \ + e-category-completion.c \ + e-category-editor.c \ + e-cell-checkbox.c \ + e-cell-combo.c \ + e-cell-date-edit.c \ + e-cell-date.c \ + e-cell-hbox.c \ + e-cell-number.c \ + e-cell-percent.c \ + e-cell-pixbuf.c \ + e-cell-popup.c \ + e-cell-renderer-color.c \ + e-cell-size.c \ + e-cell-text.c \ + e-cell-toggle.c \ + e-cell-tree.c \ + e-cell-vbox.c \ + e-cell.c \ + e-charset-combo-box.c \ + e-charset.c \ + e-client-utils.c \ + e-config.c \ + e-contact-map-window.c \ + e-contact-map.c \ + e-contact-marker.c \ + e-contact-store.c \ + e-dateedit.c \ + e-datetime-format.c \ + e-destination-store.c \ + e-dialog-utils.c \ + e-dialog-widgets.c \ + e-event.c \ + e-file-request.c \ + e-file-utils.c \ + e-filter-code.c \ + e-filter-color.c \ + e-filter-datespec.c \ + e-filter-element.c \ + e-filter-file.c \ + e-filter-input.c \ + e-filter-int.c \ + e-filter-option.c \ + e-filter-part.c \ + e-filter-rule.c \ + e-focus-tracker.c \ + e-html-utils.c \ + e-icon-factory.c \ + e-image-chooser.c \ + e-import-assistant.c \ + e-import.c \ + e-interval-chooser.c \ + e-mail-identity-combo-box.c \ + e-mail-signature-combo-box.c \ + e-mail-signature-editor.c \ + e-mail-signature-manager.c \ + e-mail-signature-preview.c \ + e-mail-signature-script-dialog.c \ + e-mail-signature-tree-view.c \ + e-map.c \ + e-marshal.c \ + e-menu-tool-action.c \ + e-menu-tool-button.c \ + e-misc-utils.c \ + e-mktemp.c \ + e-name-selector-dialog.c \ + e-name-selector-entry.c \ + e-name-selector-list.c \ + e-name-selector-model.c \ + e-name-selector.c \ + e-online-button.c \ + e-paned.c \ + e-passwords.c \ + e-picture-gallery.c \ + e-plugin-ui.c \ + e-plugin.c \ + e-poolv.c \ + e-popup-action.c \ + e-popup-menu.c \ + e-port-entry.c \ + e-preferences-window.c \ + e-preview-pane.c \ + e-print.c \ + e-printable.c \ + e-reflow-model.c \ + e-reflow.c \ + e-rule-context.c \ + e-rule-editor.c \ + e-search-bar.c \ + e-selectable.c \ + e-selection-model-array.c \ + e-selection-model-simple.c \ + e-selection-model.c \ + e-selection.c \ + e-send-options.c \ + e-sorter-array.c \ + e-sorter.c \ + e-source-combo-box.c \ + e-source-config-backend.c \ + e-source-config-dialog.c \ + e-source-config.c \ + e-source-selector-dialog.c \ + e-source-selector.c \ + e-source-util.c \ + e-spell-entry.c \ + e-stock-request.c \ + e-table-click-to-add.c \ + e-table-col.c \ + e-table-column-specification.c \ + e-table-config.c \ + e-table-extras.c \ + e-table-field-chooser-dialog.c \ + e-table-field-chooser-item.c \ + e-table-field-chooser.c \ + e-table-group-container.c \ + e-table-group-leaf.c \ + e-table-group.c \ + e-table-header-item.c \ + e-table-header-utils.c \ + e-table-header.c \ + e-table-item.c \ + e-table-memory-callbacks.c \ + e-table-memory-store.c \ + e-table-memory.c \ + e-table-model.c \ + e-table-one.c \ + e-table-search.c \ + e-table-selection-model.c \ + e-table-sort-info.c \ + e-table-sorted-variable.c \ + e-table-sorted.c \ + e-table-sorter.c \ + e-table-sorting-utils.c \ + e-table-specification.c \ + e-table-state.c \ + e-table-subset-variable.c \ + e-table-subset.c \ + e-table-utils.c \ + e-table-without.c \ + e-table.c \ + e-text-event-processor-emacs-like.c \ + e-text-event-processor.c \ + e-text-model-repos.c \ + e-text-model.c \ + e-text.c \ + e-timezone-dialog.c \ + e-tree-memory-callbacks.c \ + e-tree-memory.c \ + e-tree-model-generator.c \ + e-tree-model.c \ + e-tree-selection-model.c \ + e-tree-sorted.c \ + e-tree-table-adapter.c \ + e-tree.c \ + e-ui-manager.c \ + e-unicode.c \ + e-url-entry.c \ + e-util-enumtypes.c \ + e-util-private.h \ + e-web-view-gtkhtml.c \ + e-web-view-preview.c \ + e-web-view.c \ + e-xml-utils.c \ + ea-calendar-cell.c \ + ea-calendar-item.c \ + ea-cell-table.c \ + ea-widgets.c \ + gal-a11y-e-cell-popup.c \ + gal-a11y-e-cell-registry.c \ + gal-a11y-e-cell-toggle.c \ + gal-a11y-e-cell-tree.c \ + gal-a11y-e-cell-vbox.c \ + gal-a11y-e-cell.c \ + gal-a11y-e-table-click-to-add-factory.c \ + gal-a11y-e-table-click-to-add.c \ + gal-a11y-e-table-column-header.c \ + gal-a11y-e-table-factory.c \ + gal-a11y-e-table-item-factory.c \ + gal-a11y-e-table-item.c \ + gal-a11y-e-table.c \ + gal-a11y-e-text-factory.c \ + gal-a11y-e-text.c \ + gal-a11y-e-tree-factory.c \ + gal-a11y-e-tree.c \ + gal-a11y-util.c \ + gal-define-views-dialog.c \ + gal-define-views-model.c \ + gal-view-collection.c \ + gal-view-etable.c \ + gal-view-factory-etable.c \ + gal-view-factory.c \ + gal-view-instance-save-as-dialog.c \ + gal-view-instance.c \ + gal-view-new-dialog.c \ + gal-view.c \ + $(PLATFORM_SOURCES) \ + $(NULL) libeutil_la_LDFLAGS = -avoid-version $(NO_UNDEFINED) -libeutil_la_LIBADD = \ - $(top_builddir)/libevolution-utils/libevolution-utils.la \ - $(ICONV_LIBS) \ - $(EVOLUTION_DATA_SERVER_LIBS) \ - $(GNOME_PLATFORM_LIBS) \ - $(INTLLIBS) +libeutil_la_LIBADD = \ + $(top_builddir)/libgnomecanvas/libgnomecanvas.la \ + $(ICONV_LIBS) \ + $(EVOLUTION_DATA_SERVER_LIBS) \ + $(GNOME_PLATFORM_LIBS) \ + $(CHAMPLAIN_LIBS) \ + $(GEO_LIBS) \ + $(GTKHTML_LIBS) \ + $(INTLLIBS) \ + $(MATH_LIB) \ + $(NULL) -error_DATA = e-system.error -errordir = $(privdatadir)/errors -@EVO_PLUGIN_RULE@ +TEST_CPPFLAGS = \ + $(libeutil_la_CPPFLAGS) \ + $(NULL) + +TEST_LDADD = \ + $(top_builddir)/e-util/libeutil.la \ + $(libeutil_la_LIBADD) \ + $(NULL) + +evolution_source_viewer_CPPFLAGS = $(TEST_CPPFLAGS) +evolution_source_viewer_SOURCES = evolution-source-viewer.c +evolution_source_viewer_LDADD = $(TEST_LDADD) + +test_calendar_CPPFLAGS = $(TEST_CPPFLAGS) +test_calendar_SOURCES = test-calendar.c +test_calendar_LDADD = $(TEST_LDADD) + +test_category_completion_CPPFLAGS = $(TEST_CPPFLAGS) +test_category_completion_SOURCES = test-category-completion.c +test_category_completion_LDADD = $(TEST_LDADD) + +test_contact_store_CPPFLAGS = $(TEST_CPPFLAGS) +test_contact_store_SOURCES = test-contact-store.c +test_contact_store_LDADD = $(TEST_LDADD) + +test_dateedit_CPPFLAGS = $(TEST_CPPFLAGS) +test_dateedit_SOURCES = test-dateedit.c +test_dateedit_LDADD = $(TEST_LDADD) + +test_mail_signatures_CPPFLAGS = $(TEST_CPPFLAGS) +test_mail_signatures_SOURCES = test-mail-signatures.c +test_mail_signatures_LDADD = $(TEST_LDADD) + +test_name_selector_CPPFLAGS = $(TEST_CPPFLAGS) +test_name_selector_SOURCES = test-name-selector.c +test_name_selector_LDADD = $(TEST_LDADD) + +test_preferences_window_CPPFLAGS = $(TEST_CPPFLAGS) +test_preferences_window_SOURCES = test-preferences-window.c +test_preferences_window_LDADD = $(TEST_LDADD) + +test_source_combo_box_CPPFLAGS = $(TEST_CPPFLAGS) +test_source_combo_box_SOURCES = test-source-combo-box.c +test_source_combo_box_LDADD = $(TEST_LDADD) + +test_source_config_CPPFLAGS = $(TEST_CPPFLAGS) +test_source_config_SOURCES = test-source-config.c +test_source_config_LDADD = $(TEST_LDADD) + +test_source_selector_CPPFLAGS = $(TEST_CPPFLAGS) +test_source_selector_SOURCES = test-source-selector.c +test_source_selector_LDADD = $(TEST_LDADD) + +EXTRA_DIST = \ + e-util-enumtypes.h.template \ + e-util-enumtypes.c.template \ + e-system.error.xml \ + filter.error.xml \ + widgets.error.xml \ + e-marshal.list \ + $(ui_DATA) + $(NULL) -EXTRA_DIST = \ - e-util-enumtypes.h.template \ - e-util-enumtypes.c.template \ - e-system.error.xml \ - e-marshal.list +BUILT_SOURCES = \ + $(ENUM_GENERATED) \ + $(MARSHAL_GENERATED) \ + $(error_DATA) \ + $(NULL) -BUILT_SOURCES = $(ENUM_GENERATED) $(MARSHAL_GENERATED) $(error_DATA) -CLEANFILES = $(BUILT_SOURCES) +CLEANFILES = $(BUILT_SOURCES) dist-hook: cd $(distdir); rm -f $(BUILT_SOURCES) diff --git a/e-util/arrow-down.xpm b/e-util/arrow-down.xpm new file mode 100644 index 0000000000..f1e6cb4b3c --- /dev/null +++ b/e-util/arrow-down.xpm @@ -0,0 +1,21 @@ +/* XPM */ +static const char * arrow_down_xpm[] = { +"13 16 2 1", +" c None", +". c #FF0000", +" ... ", +" ... ", +" ... ", +" ... ", +" ... ", +" ... ", +" ... ", +" ... ", +" ... ", +".............", +" ........... ", +" ......... ", +" ....... ", +" ..... ", +" ... ", +" . "}; diff --git a/e-util/arrow-up.xpm b/e-util/arrow-up.xpm new file mode 100644 index 0000000000..0cc5b9a00c --- /dev/null +++ b/e-util/arrow-up.xpm @@ -0,0 +1,21 @@ +/* XPM */ +static const char * arrow_up_xpm[] = { +"13 16 2 1", +" c None", +". c #FF0000", +" . ", +" ... ", +" ..... ", +" ....... ", +" ......... ", +" ........... ", +".............", +" ... ", +" ... ", +" ... ", +" ... ", +" ... ", +" ... ", +" ... ", +" ... ", +" ... "}; diff --git a/e-util/check-empty.xpm b/e-util/check-empty.xpm new file mode 100644 index 0000000000..746b20234e --- /dev/null +++ b/e-util/check-empty.xpm @@ -0,0 +1,21 @@ +/* XPM */ +static const char * check_empty_xpm[] = { +"16 16 2 1", +" c None", +". c #000000", +" ", +" ", +" ............ ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" ............ ", +" ", +" "}; diff --git a/e-util/check-filled.xpm b/e-util/check-filled.xpm new file mode 100644 index 0000000000..c0468fc25b --- /dev/null +++ b/e-util/check-filled.xpm @@ -0,0 +1,21 @@ +/* XPM */ +static const char * check_filled_xpm[] = { +"16 16 2 1", +" c None", +". c #000000", +" ", +" ", +" ............ ", +" . . ", +" . . . ", +" . .. . ", +" . ... . ", +" . . ... . ", +" . .. ... . ", +" . ..... . ", +" . ... . ", +" . . . ", +" . . ", +" ............ ", +" ", +" "}; diff --git a/e-util/e-action-combo-box.c b/e-util/e-action-combo-box.c new file mode 100644 index 0000000000..0747a6ed27 --- /dev/null +++ b/e-util/e-action-combo-box.c @@ -0,0 +1,578 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-action-combo-box.c + * + * Copyright (C) 2008 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-action-combo-box.h" + +#include <glib/gi18n.h> + +#define E_ACTION_COMBO_BOX_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ACTION_COMBO_BOX, EActionComboBoxPrivate)) + +enum { + COLUMN_ACTION, + COLUMN_SORT +}; + +enum { + PROP_0, + PROP_ACTION +}; + +struct _EActionComboBoxPrivate { + GtkRadioAction *action; + GtkActionGroup *action_group; + GHashTable *index; + guint changed_handler_id; /* action::changed */ + guint group_sensitive_handler_id; /* action-group::sensitive */ + guint group_visible_handler_id; /* action-group::visible */ + gboolean group_has_icons : 1; +}; + +G_DEFINE_TYPE ( + EActionComboBox, + e_action_combo_box, + GTK_TYPE_COMBO_BOX) + +static void +action_combo_box_action_changed_cb (GtkRadioAction *action, + GtkRadioAction *current, + EActionComboBox *combo_box) +{ + GtkTreeRowReference *reference; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + gboolean valid; + + reference = g_hash_table_lookup ( + combo_box->priv->index, GINT_TO_POINTER ( + gtk_radio_action_get_current_value (current))); + g_return_if_fail (reference != NULL); + + model = gtk_tree_row_reference_get_model (reference); + path = gtk_tree_row_reference_get_path (reference); + valid = gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + g_return_if_fail (valid); + + gtk_combo_box_set_active_iter (GTK_COMBO_BOX (combo_box), &iter); +} + +static void +action_combo_box_action_group_notify_cb (GtkActionGroup *action_group, + GParamSpec *pspec, + EActionComboBox *combo_box) +{ + g_object_set ( + combo_box, "sensitive", + gtk_action_group_get_sensitive (action_group), "visible", + gtk_action_group_get_visible (action_group), NULL); +} + +static void +action_combo_box_render_pixbuf (GtkCellLayout *layout, + GtkCellRenderer *renderer, + GtkTreeModel *model, + GtkTreeIter *iter, + EActionComboBox *combo_box) +{ + GtkRadioAction *action; + gchar *icon_name; + gchar *stock_id; + gboolean sensitive; + gboolean visible; + gint width; + + gtk_tree_model_get (model, iter, COLUMN_ACTION, &action, -1); + + /* Do any of the actions have an icon? */ + if (!combo_box->priv->group_has_icons) + return; + + /* A NULL action means the row is a separator. */ + if (action == NULL) + return; + + g_object_get ( + G_OBJECT (action), + "icon-name", &icon_name, + "sensitive", &sensitive, + "stock-id", &stock_id, + "visible", &visible, + NULL); + + /* Keep the pixbuf renderer a fixed size for proper alignment. */ + gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, NULL); + + /* We can't set both "icon-name" and "stock-id" because setting + * one unsets the other. So pick the one that has a non-NULL + * value. If both are non-NULL, "stock-id" wins. */ + + if (stock_id != NULL) + g_object_set ( + G_OBJECT (renderer), + "sensitive", sensitive, + "icon-name", NULL, + "stock-id", stock_id, + "stock-size", GTK_ICON_SIZE_MENU, + "visible", visible, + "width", width, + NULL); + else + g_object_set ( + G_OBJECT (renderer), + "sensitive", sensitive, + "icon-name", icon_name, + "stock-id", NULL, + "stock-size", GTK_ICON_SIZE_MENU, + "visible", visible, + "width", width, + NULL); + + g_free (icon_name); + g_free (stock_id); +} + +static void +action_combo_box_render_text (GtkCellLayout *layout, + GtkCellRenderer *renderer, + GtkTreeModel *model, + GtkTreeIter *iter, + EActionComboBox *combo_box) +{ + GtkRadioAction *action; + gchar **strv; + gchar *label; + gboolean sensitive; + gboolean visible; + gint xpad; + + gtk_tree_model_get (model, iter, COLUMN_ACTION, &action, -1); + + /* A NULL action means the row is a separator. */ + if (action == NULL) + return; + + g_object_get ( + G_OBJECT (action), + "label", &label, + "sensitive", &sensitive, + "visible", &visible, + NULL); + + /* Strip out underscores. */ + strv = g_strsplit (label, "_", -1); + g_free (label); + label = g_strjoinv (NULL, strv); + g_strfreev (strv); + + xpad = combo_box->priv->group_has_icons ? 3 : 0; + + g_object_set ( + G_OBJECT (renderer), + "sensitive", sensitive, + "text", label, + "visible", visible, + "xpad", xpad, + NULL); + + g_free (label); +} + +static gboolean +action_combo_box_is_row_separator (GtkTreeModel *model, + GtkTreeIter *iter) +{ + GtkAction *action; + gboolean separator; + + /* NULL actions are rendered as separators. */ + gtk_tree_model_get (model, iter, COLUMN_ACTION, &action, -1); + separator = (action == NULL); + if (action != NULL) + g_object_unref (action); + + return separator; +} + +static void +action_combo_box_update_model (EActionComboBox *combo_box) +{ + GtkListStore *list_store; + GSList *list; + + g_hash_table_remove_all (combo_box->priv->index); + + if (combo_box->priv->action == NULL) { + gtk_combo_box_set_model (GTK_COMBO_BOX (combo_box), NULL); + return; + } + + /* We store values in the sort column as floats so that we can + * insert separators in between consecutive integer values and + * still maintain the proper ordering. */ + list_store = gtk_list_store_new ( + 2, GTK_TYPE_RADIO_ACTION, G_TYPE_FLOAT); + + list = gtk_radio_action_get_group (combo_box->priv->action); + combo_box->priv->group_has_icons = FALSE; + + while (list != NULL) { + GtkTreeRowReference *reference; + GtkRadioAction *action = list->data; + GtkTreePath *path; + GtkTreeIter iter; + gchar *icon_name; + gchar *stock_id; + gint value; + + g_object_get ( + action, "icon-name", &icon_name, + "stock-id", &stock_id, NULL); + combo_box->priv->group_has_icons |= + (icon_name != NULL || stock_id != NULL); + g_free (icon_name); + g_free (stock_id); + + gtk_list_store_append (list_store, &iter); + g_object_get (action, "value", &value, NULL); + gtk_list_store_set ( + list_store, &iter, COLUMN_ACTION, + list->data, COLUMN_SORT, (gfloat) value, -1); + + path = gtk_tree_model_get_path ( + GTK_TREE_MODEL (list_store), &iter); + reference = gtk_tree_row_reference_new ( + GTK_TREE_MODEL (list_store), path); + g_hash_table_insert ( + combo_box->priv->index, + GINT_TO_POINTER (value), reference); + gtk_tree_path_free (path); + + list = g_slist_next (list); + } + + gtk_tree_sortable_set_sort_column_id ( + GTK_TREE_SORTABLE (list_store), + COLUMN_SORT, GTK_SORT_ASCENDING); + gtk_combo_box_set_model ( + GTK_COMBO_BOX (combo_box), GTK_TREE_MODEL (list_store)); + + action_combo_box_action_changed_cb ( + combo_box->priv->action, + combo_box->priv->action, + combo_box); +} + +static void +action_combo_box_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTION: + e_action_combo_box_set_action ( + E_ACTION_COMBO_BOX (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +action_combo_box_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTION: + g_value_set_object ( + value, e_action_combo_box_get_action ( + E_ACTION_COMBO_BOX (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +action_combo_box_dispose (GObject *object) +{ + EActionComboBoxPrivate *priv = E_ACTION_COMBO_BOX_GET_PRIVATE (object); + + if (priv->action != NULL) { + g_object_unref (priv->action); + priv->action = NULL; + } + + if (priv->action_group != NULL) { + g_object_unref (priv->action_group); + priv->action_group = NULL; + } + + g_hash_table_remove_all (priv->index); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_action_combo_box_parent_class)->dispose (object); +} + +static void +action_combo_box_finalize (GObject *object) +{ + EActionComboBoxPrivate *priv = E_ACTION_COMBO_BOX_GET_PRIVATE (object); + + g_hash_table_destroy (priv->index); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_action_combo_box_parent_class)->finalize (object); +} + +static void +action_combo_box_constructed (GObject *object) +{ + GtkComboBox *combo_box; + GtkCellRenderer *renderer; + + combo_box = GTK_COMBO_BOX (object); + + /* This needs to happen after constructor properties are set + * so that GtkCellLayout.get_area() returns something valid. */ + + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (combo_box), renderer, FALSE); + gtk_cell_layout_set_cell_data_func ( + GTK_CELL_LAYOUT (combo_box), renderer, + (GtkCellLayoutDataFunc) action_combo_box_render_pixbuf, + combo_box, NULL); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (combo_box), renderer, TRUE); + gtk_cell_layout_set_cell_data_func ( + GTK_CELL_LAYOUT (combo_box), renderer, + (GtkCellLayoutDataFunc) action_combo_box_render_text, + combo_box, NULL); + + gtk_combo_box_set_row_separator_func ( + combo_box, (GtkTreeViewRowSeparatorFunc) + action_combo_box_is_row_separator, NULL, NULL); +} + +static void +action_combo_box_changed (GtkComboBox *combo_box) +{ + GtkRadioAction *action; + GtkTreeModel *model; + GtkTreeIter iter; + gint value; + + /* This method is virtual, so no need to chain up. */ + + if (!gtk_combo_box_get_active_iter (combo_box, &iter)) + return; + + model = gtk_combo_box_get_model (combo_box); + gtk_tree_model_get (model, &iter, COLUMN_ACTION, &action, -1); + g_object_get (action, "value", &value, NULL); + gtk_radio_action_set_current_value (action, value); +} + +static void +e_action_combo_box_class_init (EActionComboBoxClass *class) +{ + GObjectClass *object_class; + GtkComboBoxClass *combo_box_class; + + g_type_class_add_private (class, sizeof (EActionComboBoxPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = action_combo_box_set_property; + object_class->get_property = action_combo_box_get_property; + object_class->dispose = action_combo_box_dispose; + object_class->finalize = action_combo_box_finalize; + object_class->constructed = action_combo_box_constructed; + + combo_box_class = GTK_COMBO_BOX_CLASS (class); + combo_box_class->changed = action_combo_box_changed; + + g_object_class_install_property ( + object_class, + PROP_ACTION, + g_param_spec_object ( + "action", + "Action", + "A GtkRadioAction", + GTK_TYPE_RADIO_ACTION, + G_PARAM_READWRITE)); +} + +static void +e_action_combo_box_init (EActionComboBox *combo_box) +{ + combo_box->priv = E_ACTION_COMBO_BOX_GET_PRIVATE (combo_box); + + combo_box->priv->index = g_hash_table_new_full ( + g_direct_hash, g_direct_equal, + (GDestroyNotify) NULL, + (GDestroyNotify) gtk_tree_row_reference_free); +} + +GtkWidget * +e_action_combo_box_new (void) +{ + return e_action_combo_box_new_with_action (NULL); +} + +GtkWidget * +e_action_combo_box_new_with_action (GtkRadioAction *action) +{ + return g_object_new (E_TYPE_ACTION_COMBO_BOX, "action", action, NULL); +} + +GtkRadioAction * +e_action_combo_box_get_action (EActionComboBox *combo_box) +{ + g_return_val_if_fail (E_ACTION_IS_COMBO_BOX (combo_box), NULL); + + return combo_box->priv->action; +} + +void +e_action_combo_box_set_action (EActionComboBox *combo_box, + GtkRadioAction *action) +{ + g_return_if_fail (E_ACTION_IS_COMBO_BOX (combo_box)); + + if (action != NULL) + g_return_if_fail (GTK_IS_RADIO_ACTION (action)); + + if (combo_box->priv->action != NULL) { + g_signal_handler_disconnect ( + combo_box->priv->action, + combo_box->priv->changed_handler_id); + g_object_unref (combo_box->priv->action); + } + + if (combo_box->priv->action_group != NULL) { + g_signal_handler_disconnect ( + combo_box->priv->action_group, + combo_box->priv->group_sensitive_handler_id); + g_signal_handler_disconnect ( + combo_box->priv->action_group, + combo_box->priv->group_visible_handler_id); + g_object_unref (combo_box->priv->action_group); + combo_box->priv->action_group = NULL; + } + + if (action != NULL) + g_object_get ( + g_object_ref (action), "action-group", + &combo_box->priv->action_group, NULL); + + combo_box->priv->action = action; + action_combo_box_update_model (combo_box); + + if (combo_box->priv->action != NULL) + combo_box->priv->changed_handler_id = g_signal_connect ( + combo_box->priv->action, "changed", + G_CALLBACK (action_combo_box_action_changed_cb), + combo_box); + + if (combo_box->priv->action_group != NULL) { + g_object_ref (combo_box->priv->action_group); + combo_box->priv->group_sensitive_handler_id = + g_signal_connect ( + combo_box->priv->action_group, + "notify::sensitive", G_CALLBACK ( + action_combo_box_action_group_notify_cb), + combo_box); + combo_box->priv->group_visible_handler_id = + g_signal_connect ( + combo_box->priv->action_group, + "notify::visible", G_CALLBACK ( + action_combo_box_action_group_notify_cb), + combo_box); + } + + g_object_notify (G_OBJECT (combo_box), "action"); +} + +gint +e_action_combo_box_get_current_value (EActionComboBox *combo_box) +{ + g_return_val_if_fail (E_ACTION_IS_COMBO_BOX (combo_box), 0); + g_return_val_if_fail (combo_box->priv->action != NULL, 0); + + return gtk_radio_action_get_current_value (combo_box->priv->action); +} + +void +e_action_combo_box_set_current_value (EActionComboBox *combo_box, + gint current_value) +{ + g_return_if_fail (E_ACTION_IS_COMBO_BOX (combo_box)); + g_return_if_fail (combo_box->priv->action != NULL); + + gtk_radio_action_set_current_value ( + combo_box->priv->action, current_value); +} + +void +e_action_combo_box_add_separator_before (EActionComboBox *combo_box, + gint action_value) +{ + GtkTreeModel *model; + GtkTreeIter iter; + + g_return_if_fail (E_ACTION_IS_COMBO_BOX (combo_box)); + + /* NULL actions are rendered as separators. */ + model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo_box)); + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, COLUMN_ACTION, + NULL, COLUMN_SORT, (gfloat) action_value - 0.5, -1); +} + +void +e_action_combo_box_add_separator_after (EActionComboBox *combo_box, + gint action_value) +{ + GtkTreeModel *model; + GtkTreeIter iter; + + g_return_if_fail (E_ACTION_IS_COMBO_BOX (combo_box)); + + /* NULL actions are rendered as separators. */ + model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo_box)); + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, COLUMN_ACTION, + NULL, COLUMN_SORT, (gfloat) action_value + 0.5, -1); +} diff --git a/e-util/e-action-combo-box.h b/e-util/e-action-combo-box.h new file mode 100644 index 0000000000..43adeaff7a --- /dev/null +++ b/e-util/e-action-combo-box.h @@ -0,0 +1,89 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-action-combo-box.h + * + * Copyright (C) 2008 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ACTION_COMBO_BOX_H +#define E_ACTION_COMBO_BOX_H + +/* This is a GtkComboBox that is driven by a group of GtkRadioActions. + * Just plug in a GtkRadioAction and the widget will handle the rest. + * (Based on GtkhtmlComboBox.) */ + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_ACTION_COMBO_BOX \ + (e_action_combo_box_get_type ()) +#define E_ACTION_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ACTION_COMBO_BOX, EActionComboBox)) +#define E_ACTION_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ACTION_COMBO_BOX, EActionComboBoxClass)) +#define E_ACTION_IS_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ACTION_COMBO_BOX)) +#define E_ACTION_IS_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ACTION_COMBO_BOX)) +#define E_ACTION_COMBO_BOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ACTION_COMBO_BOX, EActionComboBoxClass)) + +G_BEGIN_DECLS + +typedef struct _EActionComboBox EActionComboBox; +typedef struct _EActionComboBoxClass EActionComboBoxClass; +typedef struct _EActionComboBoxPrivate EActionComboBoxPrivate; + +struct _EActionComboBox { + GtkComboBox parent; + EActionComboBoxPrivate *priv; +}; + +struct _EActionComboBoxClass { + GtkComboBoxClass parent_class; +}; + +GType e_action_combo_box_get_type (void); +GtkWidget * e_action_combo_box_new (void); +GtkWidget * e_action_combo_box_new_with_action + (GtkRadioAction *action); +GtkRadioAction *e_action_combo_box_get_action (EActionComboBox *combo_box); +void e_action_combo_box_set_action (EActionComboBox *combo_box, + GtkRadioAction *action); +gint e_action_combo_box_get_current_value + (EActionComboBox *combo_box); +void e_action_combo_box_set_current_value + (EActionComboBox *combo_box, + gint current_value); +void e_action_combo_box_add_separator_before + (EActionComboBox *combo_box, + gint action_value); +void e_action_combo_box_add_separator_after + (EActionComboBox *combo_box, + gint action_value); + +G_END_DECLS + +#endif /* E_ACTION_COMBO_BOX_H */ diff --git a/e-util/e-activity-bar.c b/e-util/e-activity-bar.c new file mode 100644 index 0000000000..f85b403bd7 --- /dev/null +++ b/e-util/e-activity-bar.c @@ -0,0 +1,366 @@ +/* + * e-activity-bar.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-activity-bar.h" + +#define E_ACTIVITY_BAR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ACTIVITY_BAR, EActivityBarPrivate)) + +#define FEEDBACK_PERIOD 1 /* seconds */ +#define COMPLETED_ICON_NAME "emblem-default" + +struct _EActivityBarPrivate { + EActivity *activity; /* weak reference */ + GtkWidget *image; /* not referenced */ + GtkWidget *label; /* not referenced */ + GtkWidget *cancel; /* not referenced */ + GtkWidget *spinner; /* not referenced */ + + /* If the user clicks the Cancel button, keep the cancelled + * EActivity object alive for a short duration so the user + * gets some visual feedback that cancellation worked. */ + guint timeout_id; +}; + +enum { + PROP_0, + PROP_ACTIVITY +}; + +G_DEFINE_TYPE ( + EActivityBar, + e_activity_bar, + GTK_TYPE_INFO_BAR) + +static void +activity_bar_feedback (EActivityBar *bar) +{ + EActivity *activity; + EActivityState state; + + activity = e_activity_bar_get_activity (bar); + g_return_if_fail (E_IS_ACTIVITY (activity)); + + state = e_activity_get_state (activity); + if (state != E_ACTIVITY_CANCELLED && state != E_ACTIVITY_COMPLETED) + return; + + if (bar->priv->timeout_id > 0) + g_source_remove (bar->priv->timeout_id); + + /* Hold a reference on the EActivity for a short + * period so the activity bar stays visible. */ + bar->priv->timeout_id = g_timeout_add_seconds_full ( + G_PRIORITY_LOW, FEEDBACK_PERIOD, (GSourceFunc) gtk_false, + g_object_ref (activity), (GDestroyNotify) g_object_unref); +} + +static void +activity_bar_update (EActivityBar *bar) +{ + EActivity *activity; + EActivityState state; + GCancellable *cancellable; + const gchar *icon_name; + gboolean sensitive; + gboolean visible; + gchar *description; + + activity = e_activity_bar_get_activity (bar); + + if (activity == NULL) { + gtk_widget_hide (GTK_WIDGET (bar)); + return; + } + + cancellable = e_activity_get_cancellable (activity); + icon_name = e_activity_get_icon_name (activity); + state = e_activity_get_state (activity); + + description = e_activity_describe (activity); + gtk_label_set_text (GTK_LABEL (bar->priv->label), description); + + if (state == E_ACTIVITY_CANCELLED) { + PangoAttribute *attr; + PangoAttrList *attr_list; + + attr_list = pango_attr_list_new (); + + attr = pango_attr_strikethrough_new (TRUE); + pango_attr_list_insert (attr_list, attr); + + gtk_label_set_attributes ( + GTK_LABEL (bar->priv->label), attr_list); + + pango_attr_list_unref (attr_list); + } else + gtk_label_set_attributes ( + GTK_LABEL (bar->priv->label), NULL); + + if (state == E_ACTIVITY_COMPLETED) + icon_name = COMPLETED_ICON_NAME; + + if (state == E_ACTIVITY_CANCELLED) { + gtk_image_set_from_stock ( + GTK_IMAGE (bar->priv->image), + GTK_STOCK_CANCEL, GTK_ICON_SIZE_BUTTON); + gtk_widget_show (bar->priv->image); + } else if (icon_name != NULL) { + gtk_image_set_from_icon_name ( + GTK_IMAGE (bar->priv->image), + icon_name, GTK_ICON_SIZE_BUTTON); + gtk_widget_show (bar->priv->image); + } else { + gtk_widget_hide (bar->priv->image); + } + + visible = (cancellable != NULL); + gtk_widget_set_visible (bar->priv->cancel, visible); + + sensitive = (state == E_ACTIVITY_RUNNING); + gtk_widget_set_sensitive (bar->priv->cancel, sensitive); + + visible = (description != NULL && *description != '\0'); + gtk_widget_set_visible (GTK_WIDGET (bar), visible); + + g_free (description); +} + +static void +activity_bar_cancel (EActivityBar *bar) +{ + EActivity *activity; + GCancellable *cancellable; + + activity = e_activity_bar_get_activity (bar); + g_return_if_fail (E_IS_ACTIVITY (activity)); + + cancellable = e_activity_get_cancellable (activity); + g_cancellable_cancel (cancellable); + + activity_bar_update (bar); +} + +static void +activity_bar_weak_notify_cb (EActivityBar *bar, + GObject *where_the_object_was) +{ + g_return_if_fail (E_IS_ACTIVITY_BAR (bar)); + + bar->priv->activity = NULL; + e_activity_bar_set_activity (bar, NULL); +} + +static void +activity_bar_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVITY: + e_activity_bar_set_activity ( + E_ACTIVITY_BAR (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +activity_bar_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVITY: + g_value_set_object ( + value, e_activity_bar_get_activity ( + E_ACTIVITY_BAR (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +activity_bar_dispose (GObject *object) +{ + EActivityBarPrivate *priv; + + priv = E_ACTIVITY_BAR_GET_PRIVATE (object); + + if (priv->timeout_id > 0) { + g_source_remove (priv->timeout_id); + priv->timeout_id = 0; + } + + if (priv->activity != NULL) { + g_signal_handlers_disconnect_matched ( + priv->activity, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_weak_unref ( + G_OBJECT (priv->activity), (GWeakNotify) + activity_bar_weak_notify_cb, object); + priv->activity = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_activity_bar_parent_class)->dispose (object); +} + +static void +e_activity_bar_class_init (EActivityBarClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EActivityBarPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = activity_bar_set_property; + object_class->get_property = activity_bar_get_property; + object_class->dispose = activity_bar_dispose; + + g_object_class_install_property ( + object_class, + PROP_ACTIVITY, + g_param_spec_object ( + "activity", + NULL, + NULL, + E_TYPE_ACTIVITY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); +} + +static void +e_activity_bar_init (EActivityBar *bar) +{ + GtkWidget *container; + GtkWidget *widget; + + bar->priv = E_ACTIVITY_BAR_GET_PRIVATE (bar); + + container = gtk_info_bar_get_content_area (GTK_INFO_BAR (bar)); + + widget = gtk_hbox_new (FALSE, 12); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_image_new (); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + bar->priv->image = widget; + + widget = gtk_spinner_new (); + gtk_spinner_start (GTK_SPINNER (widget)); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + bar->priv->spinner = widget; + + /* The spinner is only visible when the image is not. */ + g_object_bind_property ( + bar->priv->image, "visible", + bar->priv->spinner, "visible", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE | + G_BINDING_INVERT_BOOLEAN); + + widget = gtk_label_new (NULL); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_label_set_ellipsize (GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + bar->priv->label = widget; + gtk_widget_show (widget); + + /* This is only shown if the EActivity has a GCancellable. */ + widget = gtk_button_new_from_stock (GTK_STOCK_CANCEL); + gtk_info_bar_add_action_widget ( + GTK_INFO_BAR (bar), widget, GTK_RESPONSE_CANCEL); + bar->priv->cancel = widget; + gtk_widget_hide (widget); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (activity_bar_cancel), bar); +} + +GtkWidget * +e_activity_bar_new (void) +{ + return g_object_new (E_TYPE_ACTIVITY_BAR, NULL); +} + +EActivity * +e_activity_bar_get_activity (EActivityBar *bar) +{ + g_return_val_if_fail (E_IS_ACTIVITY_BAR (bar), NULL); + + return bar->priv->activity; +} + +void +e_activity_bar_set_activity (EActivityBar *bar, + EActivity *activity) +{ + g_return_if_fail (E_IS_ACTIVITY_BAR (bar)); + + if (activity != NULL) + g_return_if_fail (E_IS_ACTIVITY (activity)); + + if (bar->priv->timeout_id > 0) { + g_source_remove (bar->priv->timeout_id); + bar->priv->timeout_id = 0; + } + + if (bar->priv->activity != NULL) { + g_signal_handlers_disconnect_matched ( + bar->priv->activity, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, bar); + g_object_weak_unref ( + G_OBJECT (bar->priv->activity), + (GWeakNotify) activity_bar_weak_notify_cb, bar); + } + + bar->priv->activity = activity; + + if (activity != NULL) { + g_object_weak_ref ( + G_OBJECT (activity), (GWeakNotify) + activity_bar_weak_notify_cb, bar); + + g_signal_connect_swapped ( + activity, "notify::state", + G_CALLBACK (activity_bar_feedback), bar); + + g_signal_connect_swapped ( + activity, "notify", + G_CALLBACK (activity_bar_update), bar); + } + + activity_bar_update (bar); + + g_object_notify (G_OBJECT (bar), "activity"); +} diff --git a/e-util/e-activity-bar.h b/e-util/e-activity-bar.h new file mode 100644 index 0000000000..d56378e2c1 --- /dev/null +++ b/e-util/e-activity-bar.h @@ -0,0 +1,71 @@ +/* + * e-activity-bar.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ACTIVITY_BAR_H +#define E_ACTIVITY_BAR_H + +#include <gtk/gtk.h> +#include <e-util/e-activity.h> + +/* Standard GObject macros */ +#define E_TYPE_ACTIVITY_BAR \ + (e_activity_bar_get_type ()) +#define E_ACTIVITY_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ACTIVITY_BAR, EActivityBar)) +#define E_ACTIVITY_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ACTIVITY_BAR, EActivityBarClass)) +#define E_IS_ACTIVITY_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ACTIVITY_BAR)) +#define E_IS_ACTIVITY_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ACTIVITY_BAR)) +#define E_ACTIVITY_BAR_GET_CLASS(obj) \ + (G_TYPE_CHECK_INSTANCE_GET_TYPE \ + ((obj), E_TYPE_ACTIVITY_BAR, EActivityBarClass)) + +G_BEGIN_DECLS + +typedef struct _EActivityBar EActivityBar; +typedef struct _EActivityBarClass EActivityBarClass; +typedef struct _EActivityBarPrivate EActivityBarPrivate; + +struct _EActivityBar { + GtkInfoBar parent; + EActivityBarPrivate *priv; +}; + +struct _EActivityBarClass { + GtkInfoBarClass parent_class; +}; + +GType e_activity_bar_get_type (void); +GtkWidget * e_activity_bar_new (void); +EActivity * e_activity_bar_get_activity (EActivityBar *bar); +void e_activity_bar_set_activity (EActivityBar *bar, + EActivity *activity); + +G_END_DECLS + +#endif /* E_ACTIVITY_BAR_H */ diff --git a/e-util/e-activity-proxy.c b/e-util/e-activity-proxy.c new file mode 100644 index 0000000000..7547088aac --- /dev/null +++ b/e-util/e-activity-proxy.c @@ -0,0 +1,381 @@ +/* + * e-activity-proxy.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-activity-proxy.h" + +#include <glib/gi18n.h> + +#define E_ACTIVITY_PROXY_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ACTIVITY_PROXY, EActivityProxyPrivate)) + +#define FEEDBACK_PERIOD 1 /* seconds */ +#define COMPLETED_ICON_NAME "emblem-default" + +struct _EActivityProxyPrivate { + EActivity *activity; /* weak reference */ + GtkWidget *image; /* not referenced */ + GtkWidget *label; /* not referenced */ + GtkWidget *cancel; /* not referenced */ + GtkWidget *spinner; /* not referenced */ + + /* If the user clicks the Cancel button, keep the cancelled + * EActivity object alive for a short duration so the user + * gets some visual feedback that cancellation worked. */ + guint timeout_id; +}; + +enum { + PROP_0, + PROP_ACTIVITY +}; + +G_DEFINE_TYPE ( + EActivityProxy, + e_activity_proxy, + GTK_TYPE_FRAME) + +static void +activity_proxy_feedback (EActivityProxy *proxy) +{ + EActivity *activity; + EActivityState state; + + activity = e_activity_proxy_get_activity (proxy); + g_return_if_fail (E_IS_ACTIVITY (activity)); + + state = e_activity_get_state (activity); + if (state != E_ACTIVITY_CANCELLED) + return; + + if (proxy->priv->timeout_id > 0) + g_source_remove (proxy->priv->timeout_id); + + /* Hold a reference on the EActivity for a short + * period so the activity proxy stays visible. */ + proxy->priv->timeout_id = g_timeout_add_seconds_full ( + G_PRIORITY_LOW, FEEDBACK_PERIOD, (GSourceFunc) gtk_false, + g_object_ref (activity), (GDestroyNotify) g_object_unref); +} + +static void +activity_proxy_update (EActivityProxy *proxy) +{ + EActivity *activity; + EActivityState state; + GCancellable *cancellable; + const gchar *icon_name; + gboolean sensitive; + gboolean visible; + gchar *description; + + activity = e_activity_proxy_get_activity (proxy); + + if (activity == NULL) { + gtk_widget_hide (GTK_WIDGET (proxy)); + return; + } + + cancellable = e_activity_get_cancellable (activity); + icon_name = e_activity_get_icon_name (activity); + state = e_activity_get_state (activity); + + description = e_activity_describe (activity); + gtk_widget_set_tooltip_text (GTK_WIDGET (proxy), description); + gtk_label_set_text (GTK_LABEL (proxy->priv->label), description); + + if (state == E_ACTIVITY_CANCELLED) { + PangoAttribute *attr; + PangoAttrList *attr_list; + + attr_list = pango_attr_list_new (); + + attr = pango_attr_strikethrough_new (TRUE); + pango_attr_list_insert (attr_list, attr); + + gtk_label_set_attributes ( + GTK_LABEL (proxy->priv->label), attr_list); + + pango_attr_list_unref (attr_list); + } else + gtk_label_set_attributes ( + GTK_LABEL (proxy->priv->label), NULL); + + if (state == E_ACTIVITY_COMPLETED) + icon_name = COMPLETED_ICON_NAME; + + if (state == E_ACTIVITY_CANCELLED) { + gtk_image_set_from_stock ( + GTK_IMAGE (proxy->priv->image), + GTK_STOCK_CANCEL, GTK_ICON_SIZE_BUTTON); + gtk_widget_show (proxy->priv->image); + } else if (icon_name != NULL) { + gtk_image_set_from_icon_name ( + GTK_IMAGE (proxy->priv->image), + icon_name, GTK_ICON_SIZE_MENU); + gtk_widget_show (proxy->priv->image); + } else { + gtk_widget_hide (proxy->priv->image); + } + + visible = (cancellable != NULL); + gtk_widget_set_visible (proxy->priv->cancel, visible); + + sensitive = (state == E_ACTIVITY_RUNNING); + gtk_widget_set_sensitive (proxy->priv->cancel, sensitive); + + visible = (description != NULL && *description != '\0'); + gtk_widget_set_visible (GTK_WIDGET (proxy), visible); + + g_free (description); +} + +static void +activity_proxy_cancel (EActivityProxy *proxy) +{ + EActivity *activity; + GCancellable *cancellable; + + activity = e_activity_proxy_get_activity (proxy); + g_return_if_fail (E_IS_ACTIVITY (activity)); + + cancellable = e_activity_get_cancellable (activity); + g_cancellable_cancel (cancellable); + + activity_proxy_update (proxy); +} + +static void +activity_proxy_weak_notify_cb (EActivityProxy *proxy, + GObject *where_the_object_was) +{ + g_return_if_fail (E_IS_ACTIVITY_PROXY (proxy)); + + proxy->priv->activity = NULL; + e_activity_proxy_set_activity (proxy, NULL); +} + +static void +activity_proxy_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVITY: + e_activity_proxy_set_activity ( + E_ACTIVITY_PROXY (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +activity_proxy_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVITY: + g_value_set_object ( + value, e_activity_proxy_get_activity ( + E_ACTIVITY_PROXY (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +activity_proxy_dispose (GObject *object) +{ + EActivityProxyPrivate *priv; + + priv = E_ACTIVITY_PROXY_GET_PRIVATE (object); + + if (priv->timeout_id > 0) { + g_source_remove (priv->timeout_id); + priv->timeout_id = 0; + } + + if (priv->activity != NULL) { + g_signal_handlers_disconnect_matched ( + priv->activity, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_weak_unref ( + G_OBJECT (priv->activity), (GWeakNotify) + activity_proxy_weak_notify_cb, object); + priv->activity = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_activity_proxy_parent_class)->dispose (object); +} + +static void +e_activity_proxy_class_init (EActivityProxyClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EActivityProxyPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = activity_proxy_set_property; + object_class->get_property = activity_proxy_get_property; + object_class->dispose = activity_proxy_dispose; + + g_object_class_install_property ( + object_class, + PROP_ACTIVITY, + g_param_spec_object ( + "activity", + NULL, + NULL, + E_TYPE_ACTIVITY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); +} + +static void +e_activity_proxy_init (EActivityProxy *proxy) +{ + GtkWidget *container; + GtkWidget *widget; + + proxy->priv = E_ACTIVITY_PROXY_GET_PRIVATE (proxy); + + gtk_frame_set_shadow_type (GTK_FRAME (proxy), GTK_SHADOW_IN); + + container = GTK_WIDGET (proxy); + + widget = gtk_hbox_new (FALSE, 3); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_image_new (); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + proxy->priv->image = widget; + + widget = gtk_spinner_new (); + gtk_spinner_start (GTK_SPINNER (widget)); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 3); + proxy->priv->spinner = widget; + + /* The spinner is only visible when the image is not. */ + g_object_bind_property ( + proxy->priv->image, "visible", + proxy->priv->spinner, "visible", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE | + G_BINDING_INVERT_BOOLEAN); + + widget = gtk_label_new (NULL); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_label_set_ellipsize (GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + proxy->priv->label = widget; + gtk_widget_show (widget); + + /* This is only shown if the EActivity has a GCancellable. */ + widget = gtk_button_new (); + gtk_button_set_image ( + GTK_BUTTON (widget), gtk_image_new_from_stock ( + GTK_STOCK_CANCEL, GTK_ICON_SIZE_MENU)); + gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_set_tooltip_text (widget, _("Cancel")); + proxy->priv->cancel = widget; + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (activity_proxy_cancel), proxy); +} + +GtkWidget * +e_activity_proxy_new (EActivity *activity) +{ + g_return_val_if_fail (E_IS_ACTIVITY (activity), NULL); + + return g_object_new ( + E_TYPE_ACTIVITY_PROXY, "activity", activity, NULL); +} + +EActivity * +e_activity_proxy_get_activity (EActivityProxy *proxy) +{ + g_return_val_if_fail (E_IS_ACTIVITY_PROXY (proxy), NULL); + + return proxy->priv->activity; +} + +void +e_activity_proxy_set_activity (EActivityProxy *proxy, + EActivity *activity) +{ + g_return_if_fail (E_IS_ACTIVITY_PROXY (proxy)); + + if (activity != NULL) + g_return_if_fail (E_IS_ACTIVITY (activity)); + + if (proxy->priv->timeout_id > 0) { + g_source_remove (proxy->priv->timeout_id); + proxy->priv->timeout_id = 0; + } + + if (proxy->priv->activity != NULL) { + g_signal_handlers_disconnect_matched ( + proxy->priv->activity, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, proxy); + g_object_weak_unref ( + G_OBJECT (proxy->priv->activity), + (GWeakNotify) activity_proxy_weak_notify_cb, proxy); + } + + proxy->priv->activity = activity; + + if (activity != NULL) { + g_object_weak_ref ( + G_OBJECT (activity), (GWeakNotify) + activity_proxy_weak_notify_cb, proxy); + + g_signal_connect_swapped ( + activity, "notify::state", + G_CALLBACK (activity_proxy_feedback), proxy); + + g_signal_connect_swapped ( + activity, "notify", + G_CALLBACK (activity_proxy_update), proxy); + } + + activity_proxy_update (proxy); + + g_object_notify (G_OBJECT (proxy), "activity"); +} diff --git a/e-util/e-activity-proxy.h b/e-util/e-activity-proxy.h new file mode 100644 index 0000000000..75125351f4 --- /dev/null +++ b/e-util/e-activity-proxy.h @@ -0,0 +1,74 @@ +/* + * e-activity-proxy.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ACTIVITY_PROXY_H +#define E_ACTIVITY_PROXY_H + +#include <gtk/gtk.h> +#include <e-util/e-activity.h> + +/* Standard GObject macros */ +#define E_TYPE_ACTIVITY_PROXY \ + (e_activity_proxy_get_type ()) +#define E_ACTIVITY_PROXY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ACTIVITY_PROXY, EActivityProxy)) +#define E_ACTIVITY_PROXY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ACTIVITY_PROXY, EActivityProxyClass)) +#define E_IS_ACTIVITY_PROXY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ACTIVITY_PROXY)) +#define E_IS_ACTIVITY_PROXY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ACTIVITY_PROXY)) +#define E_ACTIVITY_PROXY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ACTIVITY_PROXY, EActivityProxyClass)) + +G_BEGIN_DECLS + +typedef struct _EActivityProxy EActivityProxy; +typedef struct _EActivityProxyClass EActivityProxyClass; +typedef struct _EActivityProxyPrivate EActivityProxyPrivate; + +struct _EActivityProxy { + GtkFrame parent; + EActivityProxyPrivate *priv; +}; + +struct _EActivityProxyClass { + GtkFrameClass parent_class; +}; + +GType e_activity_proxy_get_type (void); +GtkWidget * e_activity_proxy_new (EActivity *activity); +EActivity * e_activity_proxy_get_activity (EActivityProxy *proxy); +void e_activity_proxy_set_activity (EActivityProxy *proxy, + EActivity *activity); + +G_END_DECLS + +#endif /* E_ACTIVITY_PROXY_H */ diff --git a/e-util/e-activity.c b/e-util/e-activity.c index cd1c5699b2..5eefb652b0 100644 --- a/e-util/e-activity.c +++ b/e-util/e-activity.c @@ -29,8 +29,7 @@ #include <glib/gi18n.h> #include <camel/camel.h> -#include "e-util/e-util.h" -#include "e-util/e-util-enumtypes.h" +#include "e-util-enumtypes.h" #define E_ACTIVITY_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ diff --git a/e-util/e-activity.h b/e-util/e-activity.h index 4cc9951fde..ac380a030c 100644 --- a/e-util/e-activity.h +++ b/e-util/e-activity.h @@ -19,11 +19,16 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_ACTIVITY_H #define E_ACTIVITY_H #include <gtk/gtk.h> -#include <libevolution-utils/e-alert-sink.h> + +#include <e-util/e-alert-sink.h> #include <e-util/e-util-enums.h> /* Standard GObject macros */ diff --git a/e-util/e-alarm-selector.c b/e-util/e-alarm-selector.c new file mode 100644 index 0000000000..bdc1b7e35e --- /dev/null +++ b/e-util/e-alarm-selector.c @@ -0,0 +1,94 @@ +/* + * e-alarm-selector.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-alarm-selector.h" + +G_DEFINE_TYPE ( + EAlarmSelector, + e_alarm_selector, + E_TYPE_SOURCE_SELECTOR) + +static gboolean +alarm_selector_get_source_selected (ESourceSelector *selector, + ESource *source) +{ + ESourceAlarms *extension; + const gchar *extension_name; + + /* Make sure this source is a calendar. */ + extension_name = e_source_selector_get_extension_name (selector); + if (!e_source_has_extension (source, extension_name)) + return FALSE; + + extension_name = E_SOURCE_EXTENSION_ALARMS; + extension = e_source_get_extension (source, extension_name); + g_return_val_if_fail (E_IS_SOURCE_ALARMS (extension), FALSE); + + return e_source_alarms_get_include_me (extension); +} + +static void +alarm_selector_set_source_selected (ESourceSelector *selector, + ESource *source, + gboolean selected) +{ + ESourceAlarms *extension; + const gchar *extension_name; + + /* Make sure this source is a calendar. */ + extension_name = e_source_selector_get_extension_name (selector); + if (!e_source_has_extension (source, extension_name)) + return; + + extension_name = E_SOURCE_EXTENSION_ALARMS; + extension = e_source_get_extension (source, extension_name); + g_return_if_fail (E_IS_SOURCE_ALARMS (extension)); + + if (selected != e_source_alarms_get_include_me (extension)) { + e_source_alarms_set_include_me (extension, selected); + e_source_selector_queue_write (selector, source); + } +} + +static void +e_alarm_selector_class_init (EAlarmSelectorClass *class) +{ + ESourceSelectorClass *source_selector_class; + + source_selector_class = E_SOURCE_SELECTOR_CLASS (class); + source_selector_class->get_source_selected = + alarm_selector_get_source_selected; + source_selector_class->set_source_selected = + alarm_selector_set_source_selected; +} + +static void +e_alarm_selector_init (EAlarmSelector *selector) +{ +} + +GtkWidget * +e_alarm_selector_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_ALARM_SELECTOR, + "extension-name", E_SOURCE_EXTENSION_CALENDAR, + "registry", registry, NULL); +} diff --git a/e-util/e-alarm-selector.h b/e-util/e-alarm-selector.h new file mode 100644 index 0000000000..c545a46cf1 --- /dev/null +++ b/e-util/e-alarm-selector.h @@ -0,0 +1,67 @@ +/* + * e-alarm-selector.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ALARM_SELECTOR_H +#define E_ALARM_SELECTOR_H + +#include <e-util/e-source-selector.h> + +/* Standard GObject macros */ +#define E_TYPE_ALARM_SELECTOR \ + (e_alarm_selector_get_type ()) +#define E_ALARM_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ALARM_SELECTOR, EAlarmSelector)) +#define E_ALARM_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ALARM_SELECTOR, EAlarmSelectorClass)) +#define E_IS_ALARM_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ALARM_SELECTOR)) +#define E_IS_ALARM_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ALARM_SELECTOR)) +#define E_ALARM_SELECTOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ALARM_SELECTOR, EAlarmSelectorClass)) + +G_BEGIN_DECLS + +typedef struct _EAlarmSelector EAlarmSelector; +typedef struct _EAlarmSelectorClass EAlarmSelectorClass; +typedef struct _EAlarmSelectorPrivate EAlarmSelectorPrivate; + +struct _EAlarmSelector { + ESourceSelector parent; + EAlarmSelectorPrivate *priv; +}; + +struct _EAlarmSelectorClass { + ESourceSelectorClass parent_class; +}; + +GType e_alarm_selector_get_type (void) G_GNUC_CONST; +GtkWidget * e_alarm_selector_new (ESourceRegistry *registry); + +G_END_DECLS + +#endif /* E_ALARM_SELECTOR_H */ diff --git a/e-util/e-alert-bar.c b/e-util/e-alert-bar.c new file mode 100644 index 0000000000..2022af99f1 --- /dev/null +++ b/e-util/e-alert-bar.c @@ -0,0 +1,390 @@ +/* + * e-alert-bar.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-alert-bar.h" + +#include <config.h> +#include <glib/gi18n-lib.h> + +#define E_ALERT_BAR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ALERT_BAR, EAlertBarPrivate)) + +#define E_ALERT_BAR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ALERT_BAR, EAlertBarPrivate)) + +/* GTK_ICON_SIZE_DIALOG is a tad too big. */ +#define ICON_SIZE GTK_ICON_SIZE_DND + +/* Dismiss warnings automatically after 5 minutes. */ +#define WARNING_TIMEOUT_SECONDS (5 * 60) + +struct _EAlertBarPrivate { + GQueue alerts; + GtkWidget *image; /* not referenced */ + GtkWidget *primary_label; /* not referenced */ + GtkWidget *secondary_label; /* not referenced */ +}; + +G_DEFINE_TYPE ( + EAlertBar, + e_alert_bar, + GTK_TYPE_INFO_BAR) + +static void +alert_bar_response_close (EAlert *alert) +{ + e_alert_response (alert, GTK_RESPONSE_CLOSE); +} + +static void +alert_bar_show_alert (EAlertBar *alert_bar) +{ + GtkImage *image; + GtkInfoBar *info_bar; + GtkWidget *action_area; + GtkWidget *widget; + EAlert *alert; + GList *actions; + GList *children; + GtkMessageType message_type; + const gchar *primary_text; + const gchar *secondary_text; + const gchar *stock_id; + gboolean have_primary_text; + gboolean have_secondary_text; + gboolean visible; + gint response_id; + gchar *markup; + + info_bar = GTK_INFO_BAR (alert_bar); + action_area = gtk_info_bar_get_action_area (info_bar); + + alert = g_queue_peek_head (&alert_bar->priv->alerts); + g_return_if_fail (E_IS_ALERT (alert)); + + /* Remove all buttons from the previous alert. */ + children = gtk_container_get_children (GTK_CONTAINER (action_area)); + while (children != NULL) { + GtkWidget *child = GTK_WIDGET (children->data); + gtk_container_remove (GTK_CONTAINER (action_area), child); + children = g_list_delete_link (children, children); + } + + /* Add alert-specific buttons. */ + actions = e_alert_peek_actions (alert); + while (actions != NULL) { + /* These actions are already wired to trigger an + * EAlert::response signal when activated, which + * will in turn call gtk_info_bar_response(), so + * we can add buttons directly to the action + * area without knowning their response IDs. */ + + widget = gtk_button_new (); + + gtk_activatable_set_related_action ( + GTK_ACTIVATABLE (widget), + GTK_ACTION (actions->data)); + + gtk_box_pack_end ( + GTK_BOX (action_area), widget, FALSE, FALSE, 0); + + actions = g_list_next (actions); + } + + /* Add a dismiss button. */ + widget = gtk_button_new (); + gtk_button_set_image ( + GTK_BUTTON (widget), + gtk_image_new_from_stock ( + GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU)); + gtk_button_set_relief ( + GTK_BUTTON (widget), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text ( + widget, _("Close this message")); + gtk_box_pack_end ( + GTK_BOX (action_area), widget, FALSE, FALSE, 0); + gtk_button_box_set_child_non_homogeneous ( + GTK_BUTTON_BOX (action_area), widget, TRUE); + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (alert_bar_response_close), alert); + + primary_text = e_alert_get_primary_text (alert); + secondary_text = e_alert_get_secondary_text (alert); + + if (primary_text == NULL) + primary_text = ""; + + if (secondary_text == NULL) + secondary_text = ""; + + have_primary_text = (*primary_text != '\0'); + have_secondary_text = (*secondary_text != '\0'); + + response_id = e_alert_get_default_response (alert); + gtk_info_bar_set_default_response (info_bar, response_id); + + message_type = e_alert_get_message_type (alert); + gtk_info_bar_set_message_type (info_bar, message_type); + + widget = alert_bar->priv->primary_label; + if (have_primary_text && have_secondary_text) + markup = g_markup_printf_escaped ( + "<b>%s</b>", primary_text); + else + markup = g_markup_escape_text (primary_text, -1); + gtk_label_set_markup (GTK_LABEL (widget), markup); + gtk_widget_set_visible (widget, have_primary_text); + g_free (markup); + + widget = alert_bar->priv->secondary_label; + if (have_primary_text && have_secondary_text) + markup = g_markup_printf_escaped ( + "<small>%s</small>", secondary_text); + else + markup = g_markup_escape_text (secondary_text, -1); + gtk_label_set_markup (GTK_LABEL (widget), markup); + gtk_widget_set_visible (widget, have_secondary_text); + g_free (markup); + + stock_id = e_alert_get_stock_id (alert); + image = GTK_IMAGE (alert_bar->priv->image); + gtk_image_set_from_stock (image, stock_id, ICON_SIZE); + + /* Avoid showing an image for one-line alerts, + * which are usually questions or informational. */ + visible = have_primary_text && have_secondary_text; + gtk_widget_set_visible (alert_bar->priv->image, visible); + + gtk_widget_show (GTK_WIDGET (alert_bar)); + + /* Warnings are generally meant for transient errors. + * No need to leave them up indefinitely. Close them + * automatically if the user hasn't responded after a + * reasonable period of time has elapsed. */ + if (message_type == GTK_MESSAGE_WARNING) + e_alert_start_timer (alert, WARNING_TIMEOUT_SECONDS); +} + +static void +alert_bar_response_cb (EAlert *alert, + gint response_id, + EAlertBar *alert_bar) +{ + GQueue *queue; + EAlert *head; + gboolean was_head; + + queue = &alert_bar->priv->alerts; + head = g_queue_peek_head (queue); + was_head = (alert == head); + + g_signal_handlers_disconnect_by_func ( + alert, alert_bar_response_cb, alert_bar); + + if (g_queue_remove (queue, alert)) + g_object_unref (alert); + + if (g_queue_is_empty (queue)) + gtk_widget_hide (GTK_WIDGET (alert_bar)); + else if (was_head) { + GtkInfoBar *info_bar = GTK_INFO_BAR (alert_bar); + gtk_info_bar_response (info_bar, response_id); + alert_bar_show_alert (alert_bar); + } +} + +static void +alert_bar_dispose (GObject *object) +{ + EAlertBarPrivate *priv; + + priv = E_ALERT_BAR_GET_PRIVATE (object); + + while (!g_queue_is_empty (&priv->alerts)) { + EAlert *alert = g_queue_pop_head (&priv->alerts); + g_signal_handlers_disconnect_by_func ( + alert, alert_bar_response_cb, object); + g_object_unref (alert); + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_alert_bar_parent_class)->dispose (object); +} + +static void +alert_bar_constructed (GObject *object) +{ + EAlertBarPrivate *priv; + GtkInfoBar *info_bar; + GtkWidget *action_area; + GtkWidget *content_area; + GtkWidget *container; + GtkWidget *widget; + + priv = E_ALERT_BAR_GET_PRIVATE (object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_alert_bar_parent_class)->constructed (object); + + g_queue_init (&priv->alerts); + + info_bar = GTK_INFO_BAR (object); + action_area = gtk_info_bar_get_action_area (info_bar); + content_area = gtk_info_bar_get_content_area (info_bar); + + gtk_orientable_set_orientation ( + GTK_ORIENTABLE (action_area), GTK_ORIENTATION_HORIZONTAL); + gtk_widget_set_valign (action_area, GTK_ALIGN_START); + + container = content_area; + + widget = gtk_image_new (); + gtk_misc_set_alignment (GTK_MISC (widget), 0.5, 0.0); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + priv->image = widget; + gtk_widget_show (widget); + + widget = gtk_vbox_new (FALSE, 12); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_label_new (NULL); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_label_set_selectable (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + priv->primary_label = widget; + gtk_widget_show (widget); + + widget = gtk_label_new (NULL); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_label_set_selectable (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + priv->secondary_label = widget; + gtk_widget_show (widget); + + container = action_area; +} + +static GtkSizeRequestMode +alert_bar_get_request_mode (GtkWidget *widget) +{ + /* GtkBox does width-for-height by default. But we + * want the alert bar to be as short as possible. */ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +e_alert_bar_class_init (EAlertBarClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EAlertBarPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = alert_bar_dispose; + object_class->constructed = alert_bar_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->get_request_mode = alert_bar_get_request_mode; +} + +static void +e_alert_bar_init (EAlertBar *alert_bar) +{ + alert_bar->priv = E_ALERT_BAR_GET_PRIVATE (alert_bar); +} + +GtkWidget * +e_alert_bar_new (void) +{ + return g_object_new (E_TYPE_ALERT_BAR, NULL); +} + +void +e_alert_bar_clear (EAlertBar *alert_bar) +{ + GQueue *queue; + EAlert *alert; + + g_return_if_fail (E_IS_ALERT_BAR (alert_bar)); + + queue = &alert_bar->priv->alerts; + + while ((alert = g_queue_pop_head (queue)) != NULL) + alert_bar_response_close (alert); +} + +typedef struct { + gboolean found; + EAlert *looking_for; +} DuplicateData; + +static void +alert_bar_find_duplicate_cb (EAlert *alert, + DuplicateData *dd) +{ + g_return_if_fail (dd->looking_for != NULL); + + dd->found |= ( + e_alert_get_message_type (alert) == + e_alert_get_message_type (dd->looking_for) && + g_strcmp0 ( + e_alert_get_primary_text (alert), + e_alert_get_primary_text (dd->looking_for)) == 0 && + g_strcmp0 ( + e_alert_get_secondary_text (alert), + e_alert_get_secondary_text (dd->looking_for)) == 0); +} + +void +e_alert_bar_add_alert (EAlertBar *alert_bar, + EAlert *alert) +{ + DuplicateData dd; + + g_return_if_fail (E_IS_ALERT_BAR (alert_bar)); + g_return_if_fail (E_IS_ALERT (alert)); + + dd.found = FALSE; + dd.looking_for = alert; + + g_queue_foreach ( + &alert_bar->priv->alerts, + (GFunc) alert_bar_find_duplicate_cb, &dd); + + if (dd.found) + return; + + g_signal_connect ( + alert, "response", + G_CALLBACK (alert_bar_response_cb), alert_bar); + + g_queue_push_head (&alert_bar->priv->alerts, g_object_ref (alert)); + + alert_bar_show_alert (alert_bar); +} diff --git a/e-util/e-alert-bar.h b/e-util/e-alert-bar.h new file mode 100644 index 0000000000..ae5b315b40 --- /dev/null +++ b/e-util/e-alert-bar.h @@ -0,0 +1,72 @@ +/* + * e-alert-bar.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ALERT_BAR_H +#define E_ALERT_BAR_H + +#include <gtk/gtk.h> + +#include <e-util/e-alert.h> + +/* Standard GObject macros */ +#define E_TYPE_ALERT_BAR \ + (e_alert_bar_get_type ()) +#define E_ALERT_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ALERT_BAR, EAlertBar)) +#define E_ALERT_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ALERT_BAR, EAlertBarClass)) +#define E_IS_ALERT_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ALERT_BAR)) +#define E_IS_ALERT_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ALERT_BAR)) +#define E_ALERT_BAR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ALERT_BAR, EAlertBarClass)) + +G_BEGIN_DECLS + +typedef struct _EAlertBar EAlertBar; +typedef struct _EAlertBarClass EAlertBarClass; +typedef struct _EAlertBarPrivate EAlertBarPrivate; + +struct _EAlertBar { + GtkInfoBar parent; + EAlertBarPrivate *priv; +}; + +struct _EAlertBarClass { + GtkInfoBarClass parent_class; +}; + +GType e_alert_bar_get_type (void); +GtkWidget * e_alert_bar_new (void); +void e_alert_bar_clear (EAlertBar *alert_bar); +void e_alert_bar_add_alert (EAlertBar *alert_bar, + EAlert *alert); + +G_END_DECLS + +#endif /* E_ALERT_BAR_H */ diff --git a/e-util/e-alert-dialog.c b/e-util/e-alert-dialog.c new file mode 100644 index 0000000000..75650902ae --- /dev/null +++ b/e-util/e-alert-dialog.c @@ -0,0 +1,403 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Michael Zucchi <notzed@ximian.com> + * Jonathon Jongsma <jonathon.jongsma@collabora.co.uk> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * Copyright (C) 2009 Intel Corporation + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <glib/gi18n.h> + +#include "e-alert-dialog.h" + +#define E_ALERT_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ALERT_DIALOG, EAlertDialogPrivate)) + +struct _EAlertDialogPrivate { + GtkWidget *content_area; /* not referenced */ + EAlert *alert; +}; + +enum { + PROP_0, + PROP_ALERT +}; + +G_DEFINE_TYPE ( + EAlertDialog, + e_alert_dialog, + GTK_TYPE_DIALOG) + +static void +alert_dialog_set_alert (EAlertDialog *dialog, + EAlert *alert) +{ + g_return_if_fail (E_IS_ALERT (alert)); + g_return_if_fail (dialog->priv->alert == NULL); + + dialog->priv->alert = g_object_ref (alert); +} + +static void +alert_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ALERT: + alert_dialog_set_alert ( + E_ALERT_DIALOG (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +alert_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ALERT: + g_value_set_object ( + value, e_alert_dialog_get_alert ( + E_ALERT_DIALOG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +alert_dialog_dispose (GObject *object) +{ + EAlertDialogPrivate *priv; + + priv = E_ALERT_DIALOG_GET_PRIVATE (object); + + if (priv->alert) { + g_signal_handlers_disconnect_matched ( + priv->alert, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->alert); + priv->alert = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_alert_dialog_parent_class)->dispose (object); +} + +static void +alert_dialog_constructed (GObject *object) +{ + EAlert *alert; + EAlertDialog *dialog; + GtkWidget *action_area; + GtkWidget *content_area; + GtkWidget *container; + GtkWidget *widget; + PangoAttribute *attr; + PangoAttrList *list; + GList *actions; + const gchar *primary, *secondary; + gint default_response; + gint min_width = -1, prefer_width = -1; + gint height; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_alert_dialog_parent_class)->constructed (object); + + dialog = E_ALERT_DIALOG (object); + alert = e_alert_dialog_get_alert (dialog); + + default_response = e_alert_get_default_response (alert); + + gtk_window_set_title (GTK_WINDOW (dialog), " "); + + action_area = gtk_dialog_get_action_area (GTK_DIALOG (dialog)); + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + + gtk_widget_ensure_style (GTK_WIDGET (dialog)); + gtk_container_set_border_width (GTK_CONTAINER (action_area), 12); + gtk_container_set_border_width (GTK_CONTAINER (content_area), 0); + + gtk_window_set_destroy_with_parent (GTK_WINDOW (dialog), TRUE); + + /* Forward EAlert::response signals to GtkDialog::response. */ + g_signal_connect_swapped ( + alert, "response", + G_CALLBACK (gtk_dialog_response), dialog); + + /* Add buttons from actions. */ + actions = e_alert_peek_actions (alert); + if (!actions) { + GtkAction *action; + + /* Make sure there is at least one action, thus the dialog can be closed. */ + action = gtk_action_new ( + "alert-response-0", _("_Dismiss"), NULL, NULL); + e_alert_add_action (alert, action, GTK_RESPONSE_CLOSE); + g_object_unref (action); + + actions = e_alert_peek_actions (alert); + } + + while (actions != NULL) { + GtkWidget *button; + gpointer data; + + /* These actions are already wired to trigger an + * EAlert::response signal when activated, which + * will in turn call to gtk_dialog_response(), + * so we can add buttons directly to the action + * area without knowing their response IDs. + * (XXX Well, kind of. See below.) */ + + button = gtk_button_new (); + + gtk_widget_set_can_default (button, TRUE); + + gtk_activatable_set_related_action ( + GTK_ACTIVATABLE (button), + GTK_ACTION (actions->data)); + + gtk_box_pack_end ( + GTK_BOX (action_area), + button, FALSE, FALSE, 0); + + /* This is set in e_alert_add_action(). */ + data = g_object_get_data ( + actions->data, "e-alert-response-id"); + + /* Normally GtkDialog sets the initial focus widget to + * the button corresponding to the default response, but + * because the buttons are not directly tied to response + * IDs, we have set both the default widget and the + * initial focus widget ourselves. */ + if (GPOINTER_TO_INT (data) == default_response) { + gtk_widget_grab_default (button); + gtk_widget_grab_focus (button); + } + + actions = g_list_next (actions); + } + + widget = gtk_hbox_new (FALSE, 12); + gtk_container_set_border_width (GTK_CONTAINER (widget), 12); + gtk_box_pack_start (GTK_BOX (content_area), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = e_alert_create_image (alert, GTK_ICON_SIZE_DIALOG); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.0); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_vbox_new (FALSE, 12); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + dialog->priv->content_area = widget; + gtk_widget_show (widget); + + container = widget; + + primary = e_alert_get_primary_text (alert); + secondary = e_alert_get_secondary_text (alert); + + list = pango_attr_list_new (); + attr = pango_attr_scale_new (PANGO_SCALE_LARGE); + pango_attr_list_insert (list, attr); + attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD); + pango_attr_list_insert (list, attr); + + widget = gtk_label_new (primary); + gtk_label_set_attributes (GTK_LABEL (widget), list); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_label_set_selectable (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.0); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_set_can_focus (widget, FALSE); + gtk_widget_show (widget); + + widget = gtk_label_new (secondary); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_label_set_selectable (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.0); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_set_can_focus (widget, FALSE); + gtk_widget_show (widget); + + widget = GTK_WIDGET (dialog); + + height = gtk_widget_get_allocated_height (widget); + gtk_widget_get_preferred_width_for_height ( + widget, height, &min_width, &prefer_width); + if (min_width < prefer_width) + gtk_window_set_default_size ( + GTK_WINDOW (dialog), MIN ( + (min_width + prefer_width) / 2, + min_width * 5 / 4), -1); + + pango_attr_list_unref (list); +} + +static void +e_alert_dialog_class_init (EAlertDialogClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EAlertDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = alert_dialog_set_property; + object_class->get_property = alert_dialog_get_property; + object_class->dispose = alert_dialog_dispose; + object_class->constructed = alert_dialog_constructed; + + g_object_class_install_property ( + object_class, + PROP_ALERT, + g_param_spec_object ( + "alert", + "Alert", + "Alert to be displayed", + E_TYPE_ALERT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_alert_dialog_init (EAlertDialog *dialog) +{ + dialog->priv = E_ALERT_DIALOG_GET_PRIVATE (dialog); +} + +GtkWidget * +e_alert_dialog_new (GtkWindow *parent, + EAlert *alert) +{ + g_return_val_if_fail (E_IS_ALERT (alert), NULL); + + return g_object_new ( + E_TYPE_ALERT_DIALOG, + "alert", alert, "transient-for", parent, NULL); +} + +GtkWidget * +e_alert_dialog_new_for_args (GtkWindow *parent, + const gchar *tag, + ...) +{ + GtkWidget *dialog; + EAlert *alert; + va_list ap; + + g_return_val_if_fail (tag != NULL, NULL); + + va_start (ap, tag); + alert = e_alert_new_valist (tag, ap); + va_end (ap); + + dialog = e_alert_dialog_new (parent, alert); + + g_object_unref (alert); + + return dialog; +} + +gint +e_alert_run_dialog (GtkWindow *parent, + EAlert *alert) +{ + GtkWidget *dialog; + gint response; + + g_return_val_if_fail (E_IS_ALERT (alert), 0); + + dialog = e_alert_dialog_new (parent, alert); + response = gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + + return response; +} + +gint +e_alert_run_dialog_for_args (GtkWindow *parent, + const gchar *tag, + ...) +{ + EAlert *alert; + gint response; + va_list ap; + + g_return_val_if_fail (tag != NULL, 0); + + va_start (ap, tag); + alert = e_alert_new_valist (tag, ap); + va_end (ap); + + response = e_alert_run_dialog (parent, alert); + + g_object_unref (alert); + + return response; +} + +/** + * e_alert_dialog_get_alert: + * @dialog: an #EAlertDialog + * + * Returns the #EAlert associated with @dialog. + * + * Returns: the #EAlert associated with @dialog + **/ +EAlert * +e_alert_dialog_get_alert (EAlertDialog *dialog) +{ + g_return_val_if_fail (E_IS_ALERT_DIALOG (dialog), NULL); + + return dialog->priv->alert; +} + +/** + * e_alert_dialog_get_content_area: + * @dialog: an #EAlertDialog + * + * Returns the vertical box containing the primary and secondary labels. + * Use this to pack additional widgets into the dialog with the proper + * horizontal alignment (maintaining the left margin below the image). + * + * Returns: the content area #GtkBox + **/ +GtkWidget * +e_alert_dialog_get_content_area (EAlertDialog *dialog) +{ + g_return_val_if_fail (E_IS_ALERT_DIALOG (dialog), NULL); + + return dialog->priv->content_area; +} diff --git a/e-util/e-alert-dialog.h b/e-util/e-alert-dialog.h new file mode 100644 index 0000000000..3d2662a398 --- /dev/null +++ b/e-util/e-alert-dialog.h @@ -0,0 +1,81 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Michael Zucchi <notzed@ximian.com> + * Jonathon Jongsma <jonathon.jongsma@collabora.co.uk> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * Copyright (C) 2009 Intel Corporation + */ + +#ifndef E_ALERT_DIALOG_H +#define E_ALERT_DIALOG_H + +#include <gtk/gtk.h> + +#include <e-util/e-alert.h> + +/* Standard GObject macros */ +#define E_TYPE_ALERT_DIALOG \ + (e_alert_dialog_get_type ()) +#define E_ALERT_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ALERT_DIALOG, EAlertDialog)) +#define E_ALERT_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ALERT_DIALOG, EAlertDialogClass)) +#define E_IS_ALERT_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ALERT_DIALOG)) +#define E_IS_ALERT_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ALERT_DIALOG)) +#define E_ALERT_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ALERT_DIALOG, EAlertDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EAlertDialog EAlertDialog; +typedef struct _EAlertDialogClass EAlertDialogClass; +typedef struct _EAlertDialogPrivate EAlertDialogPrivate; + +struct _EAlertDialog { + GtkDialog parent; + EAlertDialogPrivate *priv; +}; + +struct _EAlertDialogClass { + GtkDialogClass parent_class; +}; + +GType e_alert_dialog_get_type (void); +GtkWidget * e_alert_dialog_new (GtkWindow *parent, + EAlert *alert); +GtkWidget * e_alert_dialog_new_for_args (GtkWindow *parent, + const gchar *tag, + ...) G_GNUC_NULL_TERMINATED; +gint e_alert_run_dialog (GtkWindow *parent, + EAlert *alert); +gint e_alert_run_dialog_for_args (GtkWindow *parent, + const gchar *tag, + ...) G_GNUC_NULL_TERMINATED; +EAlert * e_alert_dialog_get_alert (EAlertDialog *dialog); +GtkWidget * e_alert_dialog_get_content_area (EAlertDialog *dialog); + +G_END_DECLS + +#endif /* E_ALERT_DIALOG_H */ diff --git a/e-util/e-alert-sink.c b/e-util/e-alert-sink.c new file mode 100644 index 0000000000..3077261a90 --- /dev/null +++ b/e-util/e-alert-sink.c @@ -0,0 +1,93 @@ +/* + * e-alert-sink.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +/** + * SECTION: e-alert-sink + * @short_description: an interface to handle alerts + * @include: e-util/e-util.h + * + * A widget that implements #EAlertSink means it can handle #EAlerts, + * usually by displaying them to the user. + **/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-alert-sink.h" + +#include "e-alert-dialog.h" + +G_DEFINE_INTERFACE ( + EAlertSink, + e_alert_sink, + GTK_TYPE_WIDGET) + +static void +alert_sink_fallback (GtkWidget *widget, + EAlert *alert) +{ + GtkWidget *dialog; + gpointer parent; + + parent = gtk_widget_get_toplevel (widget); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + dialog = e_alert_dialog_new (parent, alert); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); +} + +static void +alert_sink_submit_alert (EAlertSink *alert_sink, + EAlert *alert) +{ + /* This is just a lame fallback handler. Implementors + * are strongly encouraged to override this method. */ + alert_sink_fallback (GTK_WIDGET (alert_sink), alert); +} + +static void +e_alert_sink_default_init (EAlertSinkInterface *interface) +{ + interface->submit_alert = alert_sink_submit_alert; +} + +/** + * e_alert_sink_submit_alert: + * @alert_sink: an #EAlertSink + * @alert: an #EAlert + * + * This function is a place to pass #EAlert objects. Beyond that it has no + * well-defined behavior. It's up to the widget implementing the #EAlertSink + * interface to decide what to do with them. + **/ +void +e_alert_sink_submit_alert (EAlertSink *alert_sink, + EAlert *alert) +{ + EAlertSinkInterface *interface; + + g_return_if_fail (E_IS_ALERT_SINK (alert_sink)); + g_return_if_fail (E_IS_ALERT (alert)); + + interface = E_ALERT_SINK_GET_INTERFACE (alert_sink); + g_return_if_fail (interface->submit_alert != NULL); + + interface->submit_alert (alert_sink, alert); +} diff --git a/e-util/e-alert-sink.h b/e-util/e-alert-sink.h new file mode 100644 index 0000000000..c8fd5127e7 --- /dev/null +++ b/e-util/e-alert-sink.h @@ -0,0 +1,63 @@ +/* + * e-alert-sink.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifndef E_ALERT_SINK_H +#define E_ALERT_SINK_H + +#include <gtk/gtk.h> + +#include <e-util/e-alert.h> + +/* Standard GObject macros */ +#define E_TYPE_ALERT_SINK \ + (e_alert_sink_get_type ()) +#define E_ALERT_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ALERT_SINK, EAlertSink)) +#define E_ALERT_SINK_INTERFACE(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ALERT_SINK, EAlertSinkInterface)) +#define E_IS_ALERT_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ALERT_SINK)) +#define E_IS_ALERT_SINK_INTERFACE(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ALERT_SINK)) +#define E_ALERT_SINK_GET_INTERFACE(obj) \ + (G_TYPE_INSTANCE_GET_INTERFACE \ + ((obj), E_TYPE_ALERT_SINK, EAlertSinkInterface)) + +G_BEGIN_DECLS + +typedef struct _EAlertSink EAlertSink; +typedef struct _EAlertSinkInterface EAlertSinkInterface; + +struct _EAlertSinkInterface { + GTypeInterface parent_interface; + + void (*submit_alert) (EAlertSink *alert_sink, + EAlert *alert); +}; + +GType e_alert_sink_get_type (void); +void e_alert_sink_submit_alert (EAlertSink *alert_sink, + EAlert *alert); + +G_END_DECLS + +#endif /* E_ALERT_SINK_H */ diff --git a/e-util/e-alert.c b/e-util/e-alert.c new file mode 100644 index 0000000000..5a08e07122 --- /dev/null +++ b/e-util/e-alert.c @@ -0,0 +1,1003 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Michael Zucchi <notzed@ximian.com> + * Jonathon Jongsma <jonathon.jongsma@collabora.co.uk> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * Copyright (C) 2009 Intel Corporation + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <sys/types.h> + +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include <libedataserver/libedataserver.h> + +#include "e-alert.h" +#include "e-alert-sink.h" + +#define d(x) + +#define E_ALERT_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ALERT, EAlertPrivate)) + +typedef struct _EAlertButton EAlertButton; + +struct _e_alert { + const gchar *id; + GtkMessageType message_type; + gint default_response; + const gchar *primary_text; + const gchar *secondary_text; + EAlertButton *buttons; +}; + +struct _e_alert_table { + const gchar *domain; + const gchar *translation_domain; + GHashTable *alerts; +}; + +struct _EAlertButton { + EAlertButton *next; + const gchar *stock_id; + const gchar *label; + gint response_id; +}; + +static GHashTable *alert_table; + +/* ********************************************************************** */ + +static EAlertButton default_ok_button = { + NULL, GTK_STOCK_OK, NULL, GTK_RESPONSE_OK +}; + +static struct _e_alert default_alerts[] = { + { "error", GTK_MESSAGE_ERROR, GTK_RESPONSE_OK, + "{0}", "{1}", &default_ok_button }, + { "warning", GTK_MESSAGE_WARNING, GTK_RESPONSE_OK, + "{0}", "{1}", &default_ok_button } +}; + +/* ********************************************************************** */ + +struct _EAlertPrivate { + gchar *tag; + GPtrArray *args; + gchar *primary_text; + gchar *secondary_text; + struct _e_alert *definition; + GtkMessageType message_type; + gint default_response; + guint timeout_id; + + /* It may occur to one that we could use a GtkActionGroup here, + * but we need to preserve the button order and GtkActionGroup + * uses a hash table, which does not preserve order. */ + GQueue actions; +}; + +enum { + PROP_0, + PROP_ARGS, + PROP_TAG, + PROP_MESSAGE_TYPE, + PROP_PRIMARY_TEXT, + PROP_SECONDARY_TEXT +}; + +enum { + RESPONSE, + LAST_SIGNAL +}; + +static gulong signals[LAST_SIGNAL]; + +G_DEFINE_TYPE ( + EAlert, + e_alert, + G_TYPE_OBJECT) + +static gint +map_response (const gchar *name) +{ + GEnumClass *class; + GEnumValue *value; + + class = g_type_class_ref (GTK_TYPE_RESPONSE_TYPE); + value = g_enum_get_value_by_name (class, name); + g_type_class_unref (class); + + return (value != NULL) ? value->value : 0; +} + +static GtkMessageType +map_type (const gchar *nick) +{ + GEnumClass *class; + GEnumValue *value; + + class = g_type_class_ref (GTK_TYPE_MESSAGE_TYPE); + value = g_enum_get_value_by_nick (class, nick); + g_type_class_unref (class); + + return (value != NULL) ? value->value : GTK_MESSAGE_ERROR; +} + +/* + * XML format: + * + * <error id="error-id" type="info|warning|question|error"? + * response="default_response"? > + * <primary> Primary error text.</primary>? + * <secondary> Secondary error text.</secondary>? + * <button stock="stock-button-id"? label="button label"? + * response="response_id"? /> * + * </error> + */ + +static void +e_alert_load (const gchar *path) +{ + xmlDocPtr doc = NULL; + xmlNodePtr root, error, scan; + struct _e_alert *e; + EAlertButton *lastbutton; + struct _e_alert_table *table; + gchar *tmp; + + d (printf ("loading error file %s\n", path)); + + doc = e_xml_parse_file (path); + if (doc == NULL) { + g_warning ("Error file '%s' not found", path); + return; + } + + root = xmlDocGetRootElement (doc); + if (root == NULL + || strcmp ((gchar *) root->name, "error-list") != 0 + || (tmp = (gchar *) xmlGetProp (root, (const guchar *)"domain")) == NULL) { + g_warning ("Error file '%s' invalid format", path); + xmlFreeDoc (doc); + return; + } + + table = g_hash_table_lookup (alert_table, tmp); + if (table == NULL) { + gchar *tmp2; + + table = g_malloc0 (sizeof (*table)); + table->domain = g_strdup (tmp); + table->alerts = g_hash_table_new (g_str_hash, g_str_equal); + g_hash_table_insert (alert_table, (gpointer) table->domain, table); + + tmp2 = (gchar *) xmlGetProp ( + root, (const guchar *) "translation-domain"); + if (tmp2) { + table->translation_domain = g_strdup (tmp2); + xmlFree (tmp2); + + tmp2 = (gchar *) xmlGetProp ( + root, (const guchar *) "translation-localedir"); + if (tmp2) { + bindtextdomain (table->translation_domain, tmp2); + xmlFree (tmp2); + } + } + } else + g_warning ( + "Error file '%s', domain '%s' " + "already used, merging", path, tmp); + xmlFree (tmp); + + for (error = root->children; error; error = error->next) { + if (!strcmp ((gchar *) error->name, "error")) { + tmp = (gchar *) xmlGetProp (error, (const guchar *)"id"); + if (tmp == NULL) + continue; + + e = g_malloc0 (sizeof (*e)); + e->id = g_strdup (tmp); + + xmlFree (tmp); + lastbutton = (EAlertButton *) &e->buttons; + + tmp = (gchar *) xmlGetProp (error, (const guchar *)"type"); + e->message_type = map_type (tmp); + if (tmp) + xmlFree (tmp); + + tmp = (gchar *) xmlGetProp (error, (const guchar *)"default"); + if (tmp) { + e->default_response = map_response (tmp); + xmlFree (tmp); + } + + for (scan = error->children; scan; scan = scan->next) { + if (!strcmp ((gchar *) scan->name, "primary")) { + if ((tmp = (gchar *) xmlNodeGetContent (scan))) { + e->primary_text = g_strdup ( + dgettext (table-> + translation_domain, tmp)); + xmlFree (tmp); + } + } else if (!strcmp ((gchar *) scan->name, "secondary")) { + if ((tmp = (gchar *) xmlNodeGetContent (scan))) { + e->secondary_text = g_strdup ( + dgettext (table-> + translation_domain, tmp)); + xmlFree (tmp); + } + } else if (!strcmp ((gchar *) scan->name, "button")) { + EAlertButton *button; + gchar *label = NULL; + gchar *stock_id = NULL; + + button = g_new0 (EAlertButton, 1); + tmp = (gchar *) xmlGetProp (scan, (const guchar *)"stock"); + if (tmp) { + stock_id = g_strdup (tmp); + button->stock_id = stock_id; + xmlFree (tmp); + } + tmp = (gchar *) xmlGetProp ( + scan, (xmlChar *) "label"); + if (tmp) { + label = g_strdup ( + dgettext (table-> + translation_domain, + tmp)); + button->label = label; + xmlFree (tmp); + } + tmp = (gchar *) xmlGetProp ( + scan, (xmlChar *) "response"); + if (tmp) { + button->response_id = + map_response (tmp); + xmlFree (tmp); + } + + if (stock_id == NULL && label == NULL) { + g_warning ( + "Error file '%s': " + "missing button " + "details in error " + "'%s'", path, e->id); + g_free (stock_id); + g_free (label); + g_free (button); + } else { + lastbutton->next = button; + lastbutton = button; + } + } + } + + g_hash_table_insert (table->alerts, (gpointer) e->id, e); + } + } + + xmlFreeDoc (doc); +} + +static void +e_alert_load_tables (void) +{ + GDir *dir; + const gchar *d; + gchar *base; + struct _e_alert_table *table; + gint i; + + if (alert_table != NULL) + return; + + alert_table = g_hash_table_new (g_str_hash, g_str_equal); + + /* setup system alert types */ + table = g_malloc0 (sizeof (*table)); + table->domain = "builtin"; + table->alerts = g_hash_table_new (g_str_hash, g_str_equal); + for (i = 0; i < G_N_ELEMENTS (default_alerts); i++) + g_hash_table_insert ( + table->alerts, (gpointer) + default_alerts[i].id, &default_alerts[i]); + g_hash_table_insert (alert_table, (gpointer) table->domain, table); + + /* look for installed alert tables */ + base = g_build_filename (EVOLUTION_PRIVDATADIR, "errors", NULL); + dir = g_dir_open (base, 0, NULL); + if (dir == NULL) { + g_free (base); + return; + } + + while ((d = g_dir_read_name (dir))) { + gchar *path; + + if (d[0] == '.') + continue; + + path = g_build_filename (base, d, NULL); + e_alert_load (path); + g_free (path); + } + + g_dir_close (dir); + g_free (base); +} + +static void +alert_action_activate (EAlert *alert, + GtkAction *action) +{ + GObject *object; + gpointer data; + + object = G_OBJECT (action); + data = g_object_get_data (object, "e-alert-response-id"); + e_alert_response (alert, GPOINTER_TO_INT (data)); +} + +static gchar * +alert_format_string (const gchar *format, + GPtrArray *args) +{ + GString *string; + const gchar *end, *newstart; + gint id; + + string = g_string_sized_new (strlen (format)); + + while (format + && (newstart = strchr (format, '{')) + && (end = strchr (newstart + 1, '}'))) { + g_string_append_len (string, format, newstart - format); + id = atoi (newstart + 1); + if (id < args->len) { + g_string_append (string, args->pdata[id]); + } else + g_warning ( + "Error references argument %d " + "not supplied by caller", id); + format = end + 1; + } + + g_string_append (string, format); + + return g_string_free (string, FALSE); +} + +static void +alert_set_tag (EAlert *alert, + const gchar *tag) +{ + struct _e_alert *definition; + struct _e_alert_table *table; + gchar *domain, *id; + + alert->priv->tag = g_strdup (tag); + + g_return_if_fail (alert_table); + + domain = g_alloca (strlen (tag) + 1); + strcpy (domain, tag); + id = strchr (domain, ':'); + if (id) + *id++ = 0; + else { + g_warning ("Alert tag '%s' is missing a domain", tag); + return; + } + + table = g_hash_table_lookup (alert_table, domain); + g_return_if_fail (table); + + definition = g_hash_table_lookup (table->alerts, id); + g_warn_if_fail (definition); + + alert->priv->definition = definition; +} + +static gboolean +alert_timeout_cb (EAlert *alert) +{ + e_alert_response (alert, alert->priv->default_response); + + return FALSE; +} + +static void +alert_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + EAlert *alert = (EAlert *) object; + + switch (property_id) { + case PROP_TAG: + alert_set_tag ( + E_ALERT (object), + g_value_get_string (value)); + return; + + case PROP_ARGS: + alert->priv->args = g_value_dup_boxed (value); + return; + + case PROP_MESSAGE_TYPE: + e_alert_set_message_type ( + E_ALERT (object), + g_value_get_enum (value)); + return; + + case PROP_PRIMARY_TEXT: + e_alert_set_primary_text ( + E_ALERT (object), + g_value_get_string (value)); + return; + + case PROP_SECONDARY_TEXT: + e_alert_set_secondary_text ( + E_ALERT (object), + g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +alert_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EAlert *alert = (EAlert *) object; + + switch (property_id) { + case PROP_TAG: + g_value_set_string (value, alert->priv->tag); + return; + + case PROP_ARGS: + g_value_set_boxed (value, alert->priv->args); + return; + + case PROP_MESSAGE_TYPE: + g_value_set_enum ( + value, e_alert_get_message_type ( + E_ALERT (object))); + return; + + case PROP_PRIMARY_TEXT: + g_value_set_string ( + value, e_alert_get_primary_text ( + E_ALERT (object))); + return; + + case PROP_SECONDARY_TEXT: + g_value_set_string ( + value, e_alert_get_secondary_text ( + E_ALERT (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +alert_dispose (GObject *object) +{ + EAlert *alert = E_ALERT (object); + + if (alert->priv->timeout_id > 0) { + g_source_remove (alert->priv->timeout_id); + alert->priv->timeout_id = 0; + } + + while (!g_queue_is_empty (&alert->priv->actions)) { + GtkAction *action; + + action = g_queue_pop_head (&alert->priv->actions); + g_signal_handlers_disconnect_by_func ( + action, G_CALLBACK (alert_action_activate), object); + g_object_unref (action); + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_alert_parent_class)->dispose (object); +} + +static void +alert_finalize (GObject *object) +{ + EAlertPrivate *priv; + + priv = E_ALERT_GET_PRIVATE (object); + + g_free (priv->tag); + g_free (priv->primary_text); + g_free (priv->secondary_text); + + g_ptr_array_free (priv->args, TRUE); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_alert_parent_class)->finalize (object); +} + +static void +alert_constructed (GObject *object) +{ + EAlert *alert; + EAlertButton *button; + struct _e_alert *definition; + gint ii = 0; + + alert = E_ALERT (object); + definition = alert->priv->definition; + g_return_if_fail (definition != NULL); + + e_alert_set_message_type (alert, definition->message_type); + e_alert_set_default_response (alert, definition->default_response); + + /* Build actions out of the button definitions. */ + button = definition->buttons; + while (button != NULL) { + GtkAction *action; + gchar *action_name; + + action_name = g_strdup_printf ("alert-response-%d", ii++); + + if (button->stock_id != NULL) { + action = gtk_action_new ( + action_name, NULL, NULL, button->stock_id); + e_alert_add_action ( + alert, action, button->response_id); + g_object_unref (action); + + } else if (button->label != NULL) { + action = gtk_action_new ( + action_name, button->label, NULL, NULL); + e_alert_add_action ( + alert, action, button->response_id); + g_object_unref (action); + } + + g_free (action_name); + + button = button->next; + } + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_alert_parent_class)->constructed (object); +} + +static void +e_alert_class_init (EAlertClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + g_type_class_add_private (class, sizeof (EAlertPrivate)); + + object_class->set_property = alert_set_property; + object_class->get_property = alert_get_property; + object_class->dispose = alert_dispose; + object_class->finalize = alert_finalize; + object_class->constructed = alert_constructed; + + g_object_class_install_property ( + object_class, + PROP_ARGS, + g_param_spec_boxed ( + "args", + "Arguments", + "Arguments for formatting the alert", + G_TYPE_PTR_ARRAY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_TAG, + g_param_spec_string ( + "tag", + "alert tag", + "A tag describing the alert", + "", + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_MESSAGE_TYPE, + g_param_spec_enum ( + "message-type", + NULL, + NULL, + GTK_TYPE_MESSAGE_TYPE, + GTK_MESSAGE_ERROR, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_PRIMARY_TEXT, + g_param_spec_string ( + "primary-text", + NULL, + NULL, + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SECONDARY_TEXT, + g_param_spec_string ( + "secondary-text", + NULL, + NULL, + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + signals[RESPONSE] = g_signal_new ( + "response", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EAlertClass, response), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + e_alert_load_tables (); +} + +static void +e_alert_init (EAlert *alert) +{ + alert->priv = E_ALERT_GET_PRIVATE (alert); + + g_queue_init (&alert->priv->actions); +} + +/** + * e_alert_new: + * @tag: alert identifier + * @arg0: The first argument for the alert formatter. The list must + * be NULL terminated. + * + * Creates a new EAlert. The @tag argument is used to determine + * which alert to use, it is in the format domain:alert-id. The NULL + * terminated list of arguments, starting with @arg0 is used to fill + * out the alert definition. + * + * Returns: a new #EAlert + **/ +EAlert * +e_alert_new (const gchar *tag, + ...) +{ + EAlert *e; + va_list va; + + va_start (va, tag); + e = e_alert_new_valist (tag, va); + va_end (va); + + return e; +} + +EAlert * +e_alert_new_valist (const gchar *tag, + va_list va) +{ + EAlert *alert; + GPtrArray *args; + gchar *tmp; + + args = g_ptr_array_new_with_free_func (g_free); + + tmp = va_arg (va, gchar *); + while (tmp) { + g_ptr_array_add (args, g_strdup (tmp)); + tmp = va_arg (va, gchar *); + } + + alert = e_alert_new_array (tag, args); + + g_ptr_array_unref (args); + + return alert; +} + +EAlert * +e_alert_new_array (const gchar *tag, + GPtrArray *args) +{ + return g_object_new (E_TYPE_ALERT, "tag", tag, "args", args, NULL); +} + +gint +e_alert_get_default_response (EAlert *alert) +{ + g_return_val_if_fail (E_IS_ALERT (alert), 0); + + return alert->priv->default_response; +} + +void +e_alert_set_default_response (EAlert *alert, + gint response_id) +{ + g_return_if_fail (E_IS_ALERT (alert)); + + alert->priv->default_response = response_id; +} + +GtkMessageType +e_alert_get_message_type (EAlert *alert) +{ + g_return_val_if_fail (E_IS_ALERT (alert), GTK_MESSAGE_OTHER); + + return alert->priv->message_type; +} + +void +e_alert_set_message_type (EAlert *alert, + GtkMessageType message_type) +{ + g_return_if_fail (E_IS_ALERT (alert)); + + if (alert->priv->message_type == message_type) + return; + + alert->priv->message_type = message_type; + + g_object_notify (G_OBJECT (alert), "message-type"); +} + +const gchar * +e_alert_get_primary_text (EAlert *alert) +{ + g_return_val_if_fail (E_IS_ALERT (alert), NULL); + + if (alert->priv->primary_text != NULL) + goto exit; + + if (alert->priv->definition == NULL) + goto exit; + + if (alert->priv->definition->primary_text == NULL) + goto exit; + + if (alert->priv->args == NULL) + goto exit; + + alert->priv->primary_text = alert_format_string ( + alert->priv->definition->primary_text, + alert->priv->args); + +exit: + return alert->priv->primary_text; +} + +void +e_alert_set_primary_text (EAlert *alert, + const gchar *primary_text) +{ + g_return_if_fail (E_IS_ALERT (alert)); + + if (g_strcmp0 (alert->priv->primary_text, primary_text) == 0) + return; + + g_free (alert->priv->primary_text); + alert->priv->primary_text = g_strdup (primary_text); + + g_object_notify (G_OBJECT (alert), "primary-text"); +} + +const gchar * +e_alert_get_secondary_text (EAlert *alert) +{ + g_return_val_if_fail (E_IS_ALERT (alert), NULL); + + if (alert->priv->secondary_text != NULL) + goto exit; + + if (alert->priv->definition == NULL) + goto exit; + + if (alert->priv->definition->secondary_text == NULL) + goto exit; + + if (alert->priv->args == NULL) + goto exit; + + alert->priv->secondary_text = alert_format_string ( + alert->priv->definition->secondary_text, + alert->priv->args); + +exit: + return alert->priv->secondary_text; +} + +void +e_alert_set_secondary_text (EAlert *alert, + const gchar *secondary_text) +{ + g_return_if_fail (E_IS_ALERT (alert)); + + if (g_strcmp0 (alert->priv->secondary_text, secondary_text) == 0) + return; + + g_free (alert->priv->secondary_text); + alert->priv->secondary_text = g_strdup (secondary_text); + + g_object_notify (G_OBJECT (alert), "secondary-text"); +} + +const gchar * +e_alert_get_stock_id (EAlert *alert) +{ + const gchar *stock_id; + + g_return_val_if_fail (E_IS_ALERT (alert), NULL); + + switch (e_alert_get_message_type (alert)) { + case GTK_MESSAGE_INFO: + stock_id = GTK_STOCK_DIALOG_INFO; + break; + case GTK_MESSAGE_WARNING: + stock_id = GTK_STOCK_DIALOG_WARNING; + break; + case GTK_MESSAGE_QUESTION: + stock_id = GTK_STOCK_DIALOG_QUESTION; + break; + case GTK_MESSAGE_ERROR: + stock_id = GTK_STOCK_DIALOG_ERROR; + break; + default: + stock_id = GTK_STOCK_MISSING_IMAGE; + g_warn_if_reached (); + break; + } + + return stock_id; +} + +void +e_alert_add_action (EAlert *alert, + GtkAction *action, + gint response_id) +{ + g_return_if_fail (E_IS_ALERT (alert)); + g_return_if_fail (GTK_ACTION (action)); + + g_object_set_data ( + G_OBJECT (action), "e-alert-response-id", + GINT_TO_POINTER (response_id)); + + g_signal_connect_swapped ( + action, "activate", + G_CALLBACK (alert_action_activate), alert); + + g_queue_push_tail (&alert->priv->actions, g_object_ref (action)); +} + +GList * +e_alert_peek_actions (EAlert *alert) +{ + g_return_val_if_fail (E_IS_ALERT (alert), NULL); + + return g_queue_peek_head_link (&alert->priv->actions); +} + +GtkWidget * +e_alert_create_image (EAlert *alert, + GtkIconSize size) +{ + const gchar *stock_id; + + g_return_val_if_fail (E_IS_ALERT (alert), NULL); + + stock_id = e_alert_get_stock_id (alert); + + return gtk_image_new_from_stock (stock_id, size); +} + +void +e_alert_response (EAlert *alert, + gint response_id) +{ + g_return_if_fail (E_IS_ALERT (alert)); + + g_signal_emit (alert, signals[RESPONSE], 0, response_id); +} + +/** + * e_alert_start_timer: + * @alert: an #EAlert + * @seconds: seconds until timeout occurs + * + * Starts an internal timer for @alert. When the timer expires, @alert + * will emit the default response. There is only one timer per #EAlert, + * so calling this function repeatedly on the same #EAlert will restart + * its timer each time. If @seconds is zero, the timer is cancelled and + * no response will be emitted. + **/ +void +e_alert_start_timer (EAlert *alert, + guint seconds) +{ + g_return_if_fail (E_IS_ALERT (alert)); + + if (alert->priv->timeout_id > 0) { + g_source_remove (alert->priv->timeout_id); + alert->priv->timeout_id = 0; + } + + if (seconds > 0) + alert->priv->timeout_id = g_timeout_add_seconds ( + seconds, (GSourceFunc) alert_timeout_cb, alert); +} + +void +e_alert_submit (EAlertSink *alert_sink, + const gchar *tag, + ...) +{ + va_list va; + + va_start (va, tag); + e_alert_submit_valist (alert_sink, tag, va); + va_end (va); +} + +void +e_alert_submit_valist (EAlertSink *alert_sink, + const gchar *tag, + va_list va) +{ + EAlert *alert; + + g_return_if_fail (E_IS_ALERT_SINK (alert_sink)); + g_return_if_fail (tag != NULL); + + alert = e_alert_new_valist (tag, va); + e_alert_sink_submit_alert (alert_sink, alert); + g_object_unref (alert); +} diff --git a/e-util/e-alert.h b/e-util/e-alert.h new file mode 100644 index 0000000000..f62e612235 --- /dev/null +++ b/e-util/e-alert.h @@ -0,0 +1,119 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Michael Zucchi <notzed@ximian.com> + * Jonathon Jongsma <jonathon.jongsma@collabora.co.uk> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * Copyright (C) 2009 Intel Corporation + * + */ + +#ifndef E_ALERT_H +#define E_ALERT_H + +#include <stdarg.h> +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_ALERT \ + (e_alert_get_type ()) +#define E_ALERT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ALERT, EAlert)) +#define E_ALERT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ALERT, EAlertClass)) +#define E_IS_ALERT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ALERT)) +#define E_IS_ALERT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ALERT)) +#define E_ALERT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ALERT, EAlertClass)) + +/* takes filename, returns OK if yes */ +#define E_ALERT_ASK_FILE_EXISTS_OVERWRITE \ + "system:ask-save-file-exists-overwrite" +/* takes filename, reason */ +#define E_ALERT_NO_SAVE_FILE "system:no-save-file" +/* takes filename, reason */ +#define E_ALERT_NO_LOAD_FILE "system:no-save-file" + +G_BEGIN_DECLS + +struct _EAlertSink; + +typedef struct _EAlert EAlert; +typedef struct _EAlertClass EAlertClass; +typedef struct _EAlertPrivate EAlertPrivate; + +struct _EAlert { + GObject parent; + EAlertPrivate *priv; +}; + +struct _EAlertClass { + GObjectClass parent_class; + + /* Signals */ + void (*response) (EAlert *alert, + gint response_id); +}; + +GType e_alert_get_type (void); +EAlert * e_alert_new (const gchar *tag, + ...) G_GNUC_NULL_TERMINATED; +EAlert * e_alert_new_valist (const gchar *tag, + va_list va); +EAlert * e_alert_new_array (const gchar *tag, + GPtrArray *args); +gint e_alert_get_default_response (EAlert *alert); +void e_alert_set_default_response (EAlert *alert, + gint response_id); +GtkMessageType e_alert_get_message_type (EAlert *alert); +void e_alert_set_message_type (EAlert *alert, + GtkMessageType message_type); +const gchar * e_alert_get_primary_text (EAlert *alert); +void e_alert_set_primary_text (EAlert *alert, + const gchar *primary_text); +const gchar * e_alert_get_secondary_text (EAlert *alert); +void e_alert_set_secondary_text (EAlert *alert, + const gchar *secondary_text); +const gchar * e_alert_get_stock_id (EAlert *alert); +void e_alert_add_action (EAlert *alert, + GtkAction *action, + gint response_id); +GList * e_alert_peek_actions (EAlert *alert); +GtkWidget * e_alert_create_image (EAlert *alert, + GtkIconSize size); +void e_alert_response (EAlert *alert, + gint response_id); +void e_alert_start_timer (EAlert *alert, + guint seconds); + +void e_alert_submit (struct _EAlertSink *alert_sink, + const gchar *tag, + ...) G_GNUC_NULL_TERMINATED; +void e_alert_submit_valist (struct _EAlertSink *alert_sink, + const gchar *tag, + va_list va); + +G_END_DECLS + +#endif /* E_ALERT_H */ diff --git a/e-util/e-attachment-bar.c b/e-util/e-attachment-bar.c new file mode 100644 index 0000000000..3fc4753055 --- /dev/null +++ b/e-util/e-attachment-bar.c @@ -0,0 +1,778 @@ +/* + * e-attachment-bar.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-bar.h" + +#include <glib/gi18n.h> + +#include "e-attachment-store.h" +#include "e-attachment-icon-view.h" +#include "e-attachment-tree-view.h" + +#define E_ATTACHMENT_BAR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT_BAR, EAttachmentBarPrivate)) + +#define NUM_VIEWS 2 + +struct _EAttachmentBarPrivate { + GtkTreeModel *model; + GtkWidget *vbox; + GtkWidget *expander; + GtkWidget *combo_box; + GtkWidget *icon_view; + GtkWidget *tree_view; + GtkWidget *icon_frame; + GtkWidget *tree_frame; + GtkWidget *status_icon; + GtkWidget *status_label; + GtkWidget *save_all_button; + GtkWidget *save_one_button; + + gint active_view; + guint expanded : 1; +}; + +enum { + PROP_0, + PROP_ACTIVE_VIEW, + PROP_DRAGGING, + PROP_EDITABLE, + PROP_EXPANDED, + PROP_STORE +}; + +/* Forward Declarations */ +static void e_attachment_bar_interface_init + (EAttachmentViewInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EAttachmentBar, + e_attachment_bar, + GTK_TYPE_VBOX, + G_IMPLEMENT_INTERFACE ( + E_TYPE_ATTACHMENT_VIEW, + e_attachment_bar_interface_init)) + +static void +attachment_bar_update_status (EAttachmentBar *bar) +{ + EAttachmentStore *store; + GtkActivatable *activatable; + GtkAction *action; + GtkLabel *label; + gint num_attachments; + guint64 total_size; + gchar *display_size; + gchar *markup; + + store = E_ATTACHMENT_STORE (bar->priv->model); + label = GTK_LABEL (bar->priv->status_label); + + num_attachments = e_attachment_store_get_num_attachments (store); + total_size = e_attachment_store_get_total_size (store); + display_size = g_format_size (total_size); + + if (total_size > 0) + markup = g_strdup_printf ( + "<b>%d</b> %s (%s)", num_attachments, ngettext ( + "Attachment", "Attachments", num_attachments), + display_size); + else + markup = g_strdup_printf ( + "<b>%d</b> %s", num_attachments, ngettext ( + "Attachment", "Attachments", num_attachments)); + gtk_label_set_markup (label, markup); + g_free (markup); + + activatable = GTK_ACTIVATABLE (bar->priv->save_all_button); + action = gtk_activatable_get_related_action (activatable); + gtk_action_set_visible (action, (num_attachments > 1)); + + activatable = GTK_ACTIVATABLE (bar->priv->save_one_button); + action = gtk_activatable_get_related_action (activatable); + gtk_action_set_visible (action, (num_attachments == 1)); + + g_free (display_size); +} + +static void +attachment_bar_set_store (EAttachmentBar *bar, + EAttachmentStore *store) +{ + g_return_if_fail (E_IS_ATTACHMENT_STORE (store)); + + bar->priv->model = g_object_ref (store); + + gtk_icon_view_set_model ( + GTK_ICON_VIEW (bar->priv->icon_view), + bar->priv->model); + gtk_tree_view_set_model ( + GTK_TREE_VIEW (bar->priv->tree_view), + bar->priv->model); + + g_signal_connect_object ( + bar->priv->model, "notify::num-attachments", + G_CALLBACK (attachment_bar_update_status), bar, + G_CONNECT_SWAPPED); + + g_signal_connect_object ( + bar->priv->model, "notify::total-size", + G_CALLBACK (attachment_bar_update_status), bar, + G_CONNECT_SWAPPED); + + /* Initialize */ + attachment_bar_update_status (bar); +} + +static void +attachment_bar_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVE_VIEW: + e_attachment_bar_set_active_view ( + E_ATTACHMENT_BAR (object), + g_value_get_int (value)); + return; + + case PROP_DRAGGING: + e_attachment_view_set_dragging ( + E_ATTACHMENT_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_EDITABLE: + e_attachment_view_set_editable ( + E_ATTACHMENT_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_EXPANDED: + e_attachment_bar_set_expanded ( + E_ATTACHMENT_BAR (object), + g_value_get_boolean (value)); + return; + case PROP_STORE: + attachment_bar_set_store ( + E_ATTACHMENT_BAR (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_bar_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVE_VIEW: + g_value_set_int ( + value, + e_attachment_bar_get_active_view ( + E_ATTACHMENT_BAR (object))); + return; + + case PROP_DRAGGING: + g_value_set_boolean ( + value, + e_attachment_view_get_dragging ( + E_ATTACHMENT_VIEW (object))); + return; + + case PROP_EDITABLE: + g_value_set_boolean ( + value, + e_attachment_view_get_editable ( + E_ATTACHMENT_VIEW (object))); + return; + + case PROP_EXPANDED: + g_value_set_boolean ( + value, + e_attachment_bar_get_expanded ( + E_ATTACHMENT_BAR (object))); + return; + case PROP_STORE: + g_value_set_object ( + value, + e_attachment_bar_get_store ( + E_ATTACHMENT_BAR (object))); + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_bar_dispose (GObject *object) +{ + EAttachmentBarPrivate *priv; + + priv = E_ATTACHMENT_BAR_GET_PRIVATE (object); + + if (priv->model != NULL) { + g_object_unref (priv->model); + priv->model = NULL; + } + + if (priv->vbox != NULL) { + g_object_unref (priv->vbox); + priv->vbox = NULL; + } + + if (priv->expander != NULL) { + g_object_unref (priv->expander); + priv->expander = NULL; + } + + if (priv->combo_box != NULL) { + g_object_unref (priv->combo_box); + priv->combo_box = NULL; + } + + if (priv->icon_view != NULL) { + g_object_unref (priv->icon_view); + priv->icon_view = NULL; + } + + if (priv->tree_view != NULL) { + g_object_unref (priv->tree_view); + priv->tree_view = NULL; + } + + if (priv->icon_frame != NULL) { + g_object_unref (priv->icon_frame); + priv->icon_frame = NULL; + } + + if (priv->tree_frame != NULL) { + g_object_unref (priv->tree_frame); + priv->tree_frame = NULL; + } + + if (priv->status_icon != NULL) { + g_object_unref (priv->status_icon); + priv->status_icon = NULL; + } + + if (priv->status_label != NULL) { + g_object_unref (priv->status_label); + priv->status_label = NULL; + } + + if (priv->save_all_button != NULL) { + g_object_unref (priv->save_all_button); + priv->save_all_button = NULL; + } + + if (priv->save_one_button != NULL) { + g_object_unref (priv->save_one_button); + priv->save_one_button = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_attachment_bar_parent_class)->dispose (object); +} + +static void +attachment_bar_constructed (GObject *object) +{ + EAttachmentBarPrivate *priv; + GSettings *settings; + + priv = E_ATTACHMENT_BAR_GET_PRIVATE (object); + + /* Set up property-to-property bindings. */ + + g_object_bind_property ( + object, "active-view", + priv->combo_box, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "dragging", + priv->icon_view, "dragging", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "dragging", + priv->tree_view, "dragging", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "editable", + priv->icon_view, "editable", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "editable", + priv->tree_view, "editable", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "expanded", + priv->expander, "expanded", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "expanded", + priv->combo_box, "visible", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "expanded", + priv->vbox, "visible", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + /* Set up property-to-GSettings bindings. */ + settings = g_settings_new ("org.gnome.evolution.shell"); + g_settings_bind ( + settings, "attachment-view", + object, "active-view", + G_SETTINGS_BIND_DEFAULT); + g_object_unref (settings); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_attachment_bar_parent_class)->constructed (object); +} + +static EAttachmentViewPrivate * +attachment_bar_get_private (EAttachmentView *view) +{ + EAttachmentBar *bar; + + bar = E_ATTACHMENT_BAR (view); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + + return e_attachment_view_get_private (view); +} + +static GtkTreePath * +attachment_bar_get_path_at_pos (EAttachmentView *view, + gint x, + gint y) +{ + EAttachmentBar *bar; + + bar = E_ATTACHMENT_BAR (view); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + + return e_attachment_view_get_path_at_pos (view, x, y); +} + +static EAttachmentStore * +attachment_bar_get_store (EAttachmentView *view) +{ + return e_attachment_bar_get_store (E_ATTACHMENT_BAR (view)); +} + +static GList * +attachment_bar_get_selected_paths (EAttachmentView *view) +{ + EAttachmentBar *bar; + + bar = E_ATTACHMENT_BAR (view); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + + return e_attachment_view_get_selected_paths (view); +} + +static gboolean +attachment_bar_path_is_selected (EAttachmentView *view, + GtkTreePath *path) +{ + EAttachmentBar *bar; + + bar = E_ATTACHMENT_BAR (view); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + + return e_attachment_view_path_is_selected (view, path); +} + +static void +attachment_bar_select_path (EAttachmentView *view, + GtkTreePath *path) +{ + EAttachmentBar *bar; + + bar = E_ATTACHMENT_BAR (view); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + + e_attachment_view_select_path (view, path); +} + +static void +attachment_bar_unselect_path (EAttachmentView *view, + GtkTreePath *path) +{ + EAttachmentBar *bar; + + bar = E_ATTACHMENT_BAR (view); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + + e_attachment_view_unselect_path (view, path); +} + +static void +attachment_bar_select_all (EAttachmentView *view) +{ + EAttachmentBar *bar; + + bar = E_ATTACHMENT_BAR (view); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + + e_attachment_view_select_all (view); +} + +static void +attachment_bar_unselect_all (EAttachmentView *view) +{ + EAttachmentBar *bar; + + bar = E_ATTACHMENT_BAR (view); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + + e_attachment_view_unselect_all (view); +} + +static void +attachment_bar_update_actions (EAttachmentView *view) +{ + EAttachmentBar *bar; + + bar = E_ATTACHMENT_BAR (view); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + + e_attachment_view_update_actions (view); +} + +static void +e_attachment_bar_class_init (EAttachmentBarClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EAttachmentBarPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = attachment_bar_set_property; + object_class->get_property = attachment_bar_get_property; + object_class->dispose = attachment_bar_dispose; + object_class->constructed = attachment_bar_constructed; + + g_object_class_install_property ( + object_class, + PROP_ACTIVE_VIEW, + g_param_spec_int ( + "active-view", + "Active View", + NULL, + 0, + NUM_VIEWS, + 0, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_EXPANDED, + g_param_spec_boolean ( + "expanded", + "Expanded", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_STORE, + g_param_spec_object ( + "store", + "Attachment Store", + NULL, + E_TYPE_ATTACHMENT_STORE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); + + g_object_class_override_property ( + object_class, PROP_DRAGGING, "dragging"); + + g_object_class_override_property ( + object_class, PROP_EDITABLE, "editable"); +} + +static void +e_attachment_bar_interface_init (EAttachmentViewInterface *interface) +{ + interface->get_private = attachment_bar_get_private; + interface->get_store = attachment_bar_get_store; + interface->get_path_at_pos = attachment_bar_get_path_at_pos; + interface->get_selected_paths = attachment_bar_get_selected_paths; + interface->path_is_selected = attachment_bar_path_is_selected; + interface->select_path = attachment_bar_select_path; + interface->unselect_path = attachment_bar_unselect_path; + interface->select_all = attachment_bar_select_all; + interface->unselect_all = attachment_bar_unselect_all; + interface->update_actions = attachment_bar_update_actions; +} + +static void +e_attachment_bar_init (EAttachmentBar *bar) +{ + EAttachmentView *view; + GtkSizeGroup *size_group; + GtkWidget *container; + GtkWidget *widget; + GtkAction *action; + + bar->priv = E_ATTACHMENT_BAR_GET_PRIVATE (bar); + + gtk_box_set_spacing (GTK_BOX (bar), 6); + + /* Keep the expander label and save button the same height. */ + size_group = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL); + + /* Construct the Attachment Views */ + + container = GTK_WIDGET (bar); + + widget = gtk_vbox_new (FALSE, 0); + gtk_box_pack_end (GTK_BOX (container), widget, FALSE, FALSE, 0); + bar->priv->vbox = g_object_ref (widget); + gtk_widget_show (widget); + + container = bar->priv->vbox; + + widget = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (widget), GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + bar->priv->icon_frame = g_object_ref (widget); + gtk_widget_show (widget); + + container = widget; + + widget = e_attachment_icon_view_new (); + gtk_widget_set_can_focus (widget, TRUE); + gtk_icon_view_set_model (GTK_ICON_VIEW (widget), bar->priv->model); + gtk_container_add (GTK_CONTAINER (container), widget); + bar->priv->icon_view = g_object_ref (widget); + gtk_widget_show (widget); + + container = bar->priv->vbox; + + widget = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (widget), GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + bar->priv->tree_frame = g_object_ref (widget); + gtk_widget_hide (widget); + + container = widget; + + widget = e_attachment_tree_view_new (); + gtk_widget_set_can_focus (widget, TRUE); + gtk_tree_view_set_model (GTK_TREE_VIEW (widget), bar->priv->model); + gtk_container_add (GTK_CONTAINER (container), widget); + bar->priv->tree_view = g_object_ref (widget); + gtk_widget_show (widget); + + /* Construct the Controls */ + + container = GTK_WIDGET (bar); + + widget = gtk_hbox_new (FALSE, 12); + gtk_box_pack_end (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_expander_new (NULL); + gtk_expander_set_spacing (GTK_EXPANDER (widget), 0); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + bar->priv->expander = g_object_ref (widget); + gtk_widget_show (widget); + + /* The "Save All" button proxies the "save-all" action from + * one of the two attachment views. Doesn't matter which. */ + widget = gtk_button_new (); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + action = e_attachment_view_get_action (view, "save-all"); + gtk_button_set_image (GTK_BUTTON (widget), gtk_image_new ()); + gtk_activatable_set_related_action (GTK_ACTIVATABLE (widget), action); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + bar->priv->save_all_button = g_object_ref (widget); + gtk_widget_show (widget); + + /* Same deal with the "Save" button. */ + widget = gtk_button_new (); + view = E_ATTACHMENT_VIEW (bar->priv->icon_view); + action = e_attachment_view_get_action (view, "save-one"); + gtk_button_set_image (GTK_BUTTON (widget), gtk_image_new ()); + gtk_activatable_set_related_action (GTK_ACTIVATABLE (widget), action); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + bar->priv->save_one_button = g_object_ref (widget); + gtk_widget_show (widget); + + widget = gtk_alignment_new (1.0, 0.5, 0.0, 0.0); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_combo_box_text_new (); + gtk_size_group_add_widget (size_group, widget); + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (widget), _("Icon View")); + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (widget), _("List View")); + gtk_container_add (GTK_CONTAINER (container), widget); + bar->priv->combo_box = g_object_ref (widget); + gtk_widget_show (widget); + + container = bar->priv->expander; + + widget = gtk_hbox_new (FALSE, 6); + gtk_size_group_add_widget (size_group, widget); + gtk_expander_set_label_widget (GTK_EXPANDER (container), widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_image_new_from_icon_name ( + "mail-attachment", GTK_ICON_SIZE_MENU); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + bar->priv->status_icon = g_object_ref (widget); + gtk_widget_show (widget); + + widget = gtk_label_new (NULL); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + bar->priv->status_label = g_object_ref (widget); + gtk_widget_show (widget); + + g_object_unref (size_group); +} + +GtkWidget * +e_attachment_bar_new (EAttachmentStore *store) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), NULL); + + return g_object_new ( + E_TYPE_ATTACHMENT_BAR, + "editable", FALSE, + "store", store, NULL); +} + +gint +e_attachment_bar_get_active_view (EAttachmentBar *bar) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_BAR (bar), 0); + + return bar->priv->active_view; +} + +void +e_attachment_bar_set_active_view (EAttachmentBar *bar, + gint active_view) +{ + EAttachmentView *source; + EAttachmentView *target; + + g_return_if_fail (E_IS_ATTACHMENT_BAR (bar)); + g_return_if_fail (active_view >= 0 && active_view < NUM_VIEWS); + + if (active_view == bar->priv->active_view) + return; + + bar->priv->active_view = active_view; + + if (active_view == 0) { + gtk_widget_show (bar->priv->icon_frame); + gtk_widget_hide (bar->priv->tree_frame); + } else { + gtk_widget_hide (bar->priv->icon_frame); + gtk_widget_show (bar->priv->tree_frame); + } + + /* Synchronize the item selection of the view we're + * switching TO with the view we're switching FROM. */ + if (active_view == 0) { + /* from tree view to icon view */ + source = E_ATTACHMENT_VIEW (bar->priv->tree_view); + target = E_ATTACHMENT_VIEW (bar->priv->icon_view); + } else { + /* from icon view to tree view */ + source = E_ATTACHMENT_VIEW (bar->priv->icon_view); + target = E_ATTACHMENT_VIEW (bar->priv->tree_view); + } + + e_attachment_view_sync_selection (source, target); + + g_object_notify (G_OBJECT (bar), "active-view"); +} + +gboolean +e_attachment_bar_get_expanded (EAttachmentBar *bar) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_BAR (bar), FALSE); + + return bar->priv->expanded; +} + +void +e_attachment_bar_set_expanded (EAttachmentBar *bar, + gboolean expanded) +{ + g_return_if_fail (E_IS_ATTACHMENT_BAR (bar)); + + if (bar->priv->expanded == expanded) + return; + + bar->priv->expanded = expanded; + + g_object_notify (G_OBJECT (bar), "expanded"); +} + +EAttachmentStore * +e_attachment_bar_get_store (EAttachmentBar *bar) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_BAR (bar), NULL); + + return E_ATTACHMENT_STORE (bar->priv->model); +} diff --git a/e-util/e-attachment-bar.h b/e-util/e-attachment-bar.h new file mode 100644 index 0000000000..9f35ae2aba --- /dev/null +++ b/e-util/e-attachment-bar.h @@ -0,0 +1,83 @@ +/* + * e-attachment-bar.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_BAR_H +#define E_ATTACHMENT_BAR_H + +#include <gtk/gtk.h> +#include <e-util/e-attachment-view.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_BAR \ + (e_attachment_bar_get_type ()) +#define E_ATTACHMENT_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_BAR, EAttachmentBar)) +#define E_ATTACHMENT_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_BAR, EAttachmentBarClass)) +#define E_IS_ATTACHMENT_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_BAR)) +#define E_IS_ATTACHMENT_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_BAR)) +#define E_ATTACHMENT_BAR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_BAR, EAttachmentBarClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentBar EAttachmentBar; +typedef struct _EAttachmentBarClass EAttachmentBarClass; +typedef struct _EAttachmentBarPrivate EAttachmentBarPrivate; + +struct _EAttachmentBar { + GtkBox parent; + EAttachmentBarPrivate *priv; +}; + +struct _EAttachmentBarClass { + GtkBoxClass parent_class; +}; + +GType e_attachment_bar_get_type (void); +GtkWidget * e_attachment_bar_new (EAttachmentStore *store); +gint e_attachment_bar_get_active_view + (EAttachmentBar *bar); +void e_attachment_bar_set_active_view + (EAttachmentBar *bar, + gint active_view); +gboolean e_attachment_bar_get_expanded + (EAttachmentBar *bar); +void e_attachment_bar_set_expanded + (EAttachmentBar *bar, + gboolean expanded); +EAttachmentStore * + e_attachment_bar_get_store (EAttachmentBar *bar); + +G_END_DECLS + +#endif /* E_ATTACHMENT_BAR_H */ diff --git a/e-util/e-attachment-button.c b/e-util/e-attachment-button.c new file mode 100644 index 0000000000..a2057e3354 --- /dev/null +++ b/e-util/e-attachment-button.c @@ -0,0 +1,868 @@ +/* + * e-attachment-button.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* Much of the popup menu logic here was ripped from GtkMenuToolButton. */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-button.h" + +#define E_ATTACHMENT_BUTTON_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT_BUTTON, EAttachmentButtonPrivate)) + +struct _EAttachmentButtonPrivate { + + EAttachmentView *view; + EAttachment *attachment; + gulong reference_handler_id; + + GBinding *can_show_binding; + GBinding *shown_binding; + + GtkWidget *expand_button; + GtkWidget *toggle_button; + GtkWidget *cell_view; + GtkWidget *popup_menu; + + guint expandable : 1; + guint expanded : 1; +}; + +enum { + PROP_0, + PROP_ATTACHMENT, + PROP_EXPANDABLE, + PROP_EXPANDED, + PROP_VIEW +}; + +G_DEFINE_TYPE ( + EAttachmentButton, + e_attachment_button, + GTK_TYPE_HBOX) + +static void +attachment_button_menu_deactivate_cb (EAttachmentButton *button) +{ + EAttachmentView *view; + GtkActionGroup *action_group; + GtkToggleButton *toggle_button; + + view = e_attachment_button_get_view (button); + action_group = e_attachment_view_get_action_group (view, "inline"); + toggle_button = GTK_TOGGLE_BUTTON (button->priv->toggle_button); + + gtk_toggle_button_set_active (toggle_button, FALSE); + + gtk_action_group_set_visible (action_group, FALSE); +} + +static void +attachment_button_menu_position (GtkMenu *menu, + gint *x, + gint *y, + gboolean *push_in, + EAttachmentButton *button) +{ + GtkRequisition menu_requisition; + GtkTextDirection direction; + GtkAllocation allocation; + GdkRectangle monitor; + GdkScreen *screen; + GdkWindow *window; + GtkWidget *widget; + GtkWidget *toggle_button; + gint monitor_num; + + widget = GTK_WIDGET (button); + toggle_button = button->priv->toggle_button; + gtk_widget_get_preferred_size (GTK_WIDGET (menu), &menu_requisition, NULL); + + window = gtk_widget_get_parent_window (widget); + screen = gtk_widget_get_screen (GTK_WIDGET (menu)); + monitor_num = gdk_screen_get_monitor_at_window (screen, window); + if (monitor_num < 0) + monitor_num = 0; + gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor); + + gtk_widget_get_allocation (widget, &allocation); + + gdk_window_get_origin (window, x, y); + *x += allocation.x; + *y += allocation.y; + + direction = gtk_widget_get_direction (widget); + if (direction == GTK_TEXT_DIR_LTR) + *x += MAX (allocation.width - menu_requisition.width, 0); + else if (menu_requisition.width > allocation.width) + *x -= menu_requisition.width - allocation.width; + + gtk_widget_get_allocation (toggle_button, &allocation); + + if ((*y + allocation.height + + menu_requisition.height) <= monitor.y + monitor.height) + *y += allocation.height; + else if ((*y - menu_requisition.height) >= monitor.y) + *y -= menu_requisition.height; + else if (monitor.y + monitor.height - + (*y + allocation.height) > *y) + *y += allocation.height; + else + *y -= menu_requisition.height; + + *push_in = FALSE; +} + +static void +attachment_button_select_path (EAttachmentButton *button) +{ + EAttachmentView *view; + EAttachment *attachment; + GtkTreeRowReference *reference; + GtkTreePath *path; + + attachment = e_attachment_button_get_attachment (button); + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + reference = e_attachment_get_reference (attachment); + g_return_if_fail (gtk_tree_row_reference_valid (reference)); + + view = e_attachment_button_get_view (button); + path = gtk_tree_row_reference_get_path (reference); + + e_attachment_view_unselect_all (view); + e_attachment_view_select_path (view, path); + + gtk_tree_path_free (path); +} + +static void +attachment_button_show_popup_menu (EAttachmentButton *button, + GdkEventButton *event) +{ + EAttachmentView *view; + GtkActionGroup *action_group; + GtkToggleButton *toggle_button; + + view = e_attachment_button_get_view (button); + action_group = e_attachment_view_get_action_group (view, "inline"); + toggle_button = GTK_TOGGLE_BUTTON (button->priv->toggle_button); + + attachment_button_select_path (button); + gtk_toggle_button_set_active (toggle_button, TRUE); + + e_attachment_view_show_popup_menu ( + view, event, (GtkMenuPositionFunc) + attachment_button_menu_position, button); + + gtk_action_group_set_visible (action_group, TRUE); +} + +static void +attachment_button_update_cell_view (EAttachmentButton *button) +{ + GtkCellView *cell_view; + EAttachment *attachment; + GtkTreeRowReference *reference; + GtkTreeModel *model = NULL; + GtkTreePath *path = NULL; + + cell_view = GTK_CELL_VIEW (button->priv->cell_view); + + attachment = e_attachment_button_get_attachment (button); + if (attachment == NULL) + goto exit; + + reference = e_attachment_get_reference (attachment); + if (reference == NULL) + goto exit; + + model = gtk_tree_row_reference_get_model (reference); + path = gtk_tree_row_reference_get_path (reference); + +exit: + gtk_cell_view_set_model (cell_view, model); + gtk_cell_view_set_displayed_row (cell_view, path); + + if (path != NULL) + gtk_tree_path_free (path); +} + +static void +attachment_button_update_pixbufs (EAttachmentButton *button) +{ + GtkCellLayout *cell_layout; + GtkCellRenderer *renderer; + GdkPixbuf *pixbuf_expander_open; + GdkPixbuf *pixbuf_expander_closed; + GList *list; + + /* Grab the first cell renderer. */ + cell_layout = GTK_CELL_LAYOUT (button->priv->cell_view); + list = gtk_cell_layout_get_cells (cell_layout); + renderer = GTK_CELL_RENDERER (list->data); + g_list_free (list); + + pixbuf_expander_open = gtk_widget_render_icon ( + GTK_WIDGET (button), GTK_STOCK_GO_DOWN, + GTK_ICON_SIZE_BUTTON, NULL); + + pixbuf_expander_closed = gtk_widget_render_icon ( + GTK_WIDGET (button), GTK_STOCK_GO_FORWARD, + GTK_ICON_SIZE_BUTTON, NULL); + + g_object_set ( + renderer, + "pixbuf-expander-open", pixbuf_expander_open, + "pixbuf-expander-closed", pixbuf_expander_closed, + NULL); + + g_object_unref (pixbuf_expander_open); + g_object_unref (pixbuf_expander_closed); +} + +static void +attachment_button_expand_clicked_cb (EAttachmentButton *button) +{ + gboolean expanded; + + expanded = e_attachment_button_get_expanded (button); + e_attachment_button_set_expanded (button, !expanded); +} + +static void +attachment_button_expand_drag_begin_cb (EAttachmentButton *button, + GdkDragContext *context) +{ + EAttachmentView *view; + + view = e_attachment_button_get_view (button); + + attachment_button_select_path (button); + e_attachment_view_drag_begin (view, context); +} + +static void +attachment_button_expand_drag_data_get_cb (EAttachmentButton *button, + GdkDragContext *context, + GtkSelectionData *selection, + guint info, + guint time) +{ + EAttachmentView *view; + + if (button->priv->attachment) { + gchar *mime_type; + + mime_type = e_attachment_get_mime_type ( + button->priv->attachment); + + if (mime_type) { + gboolean processed = FALSE; + GdkAtom atom; + gchar *atom_name; + + atom = gtk_selection_data_get_target (selection); + atom_name = gdk_atom_name (atom); + + if (g_strcmp0 (atom_name, mime_type) == 0) { + CamelMimePart *mime_part; + + mime_part = e_attachment_get_mime_part ( + button->priv->attachment); + + if (CAMEL_IS_MIME_PART (mime_part)) { + CamelDataWrapper *wrapper; + CamelStream *stream; + GByteArray *buffer; + + buffer = g_byte_array_new (); + stream = camel_stream_mem_new (); + camel_stream_mem_set_byte_array ( + CAMEL_STREAM_MEM (stream), + buffer); + wrapper = camel_medium_get_content ( + CAMEL_MEDIUM (mime_part)); + camel_data_wrapper_decode_to_stream_sync ( + wrapper, stream, NULL, NULL); + g_object_unref (stream); + + gtk_selection_data_set ( + selection, atom, 8, + buffer->data, buffer->len); + processed = TRUE; + + g_byte_array_free (buffer, TRUE); + } + } + + g_free (atom_name); + g_free (mime_type); + + if (processed) + return; + } + } + + view = e_attachment_button_get_view (button); + + e_attachment_view_drag_data_get ( + view, context, selection, info, time); +} + +static void +attachment_button_expand_drag_end_cb (EAttachmentButton *button, + GdkDragContext *context) +{ + EAttachmentView *view; + + view = e_attachment_button_get_view (button); + + e_attachment_view_drag_end (view, context); +} + +static gboolean +attachment_button_toggle_button_press_event_cb (EAttachmentButton *button, + GdkEventButton *event) +{ + if (event->button == 1) { + attachment_button_show_popup_menu (button, event); + return TRUE; + } + + return FALSE; +} + +static void +attachment_button_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ATTACHMENT: + e_attachment_button_set_attachment ( + E_ATTACHMENT_BUTTON (object), + g_value_get_object (value)); + return; + + case PROP_EXPANDABLE: + e_attachment_button_set_expandable ( + E_ATTACHMENT_BUTTON (object), + g_value_get_boolean (value)); + return; + + case PROP_EXPANDED: + e_attachment_button_set_expanded ( + E_ATTACHMENT_BUTTON (object), + g_value_get_boolean (value)); + return; + + case PROP_VIEW: + e_attachment_button_set_view ( + E_ATTACHMENT_BUTTON (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_button_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ATTACHMENT: + g_value_set_object ( + value, + e_attachment_button_get_attachment ( + E_ATTACHMENT_BUTTON (object))); + return; + + case PROP_EXPANDABLE: + g_value_set_boolean ( + value, + e_attachment_button_get_expandable ( + E_ATTACHMENT_BUTTON (object))); + return; + + case PROP_EXPANDED: + g_value_set_boolean ( + value, + e_attachment_button_get_expanded ( + E_ATTACHMENT_BUTTON (object))); + return; + + case PROP_VIEW: + g_value_set_object ( + value, + e_attachment_button_get_view ( + E_ATTACHMENT_BUTTON (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_button_dispose (GObject *object) +{ + EAttachmentButtonPrivate *priv; + + priv = E_ATTACHMENT_BUTTON_GET_PRIVATE (object); + + if (priv->view != NULL) { + g_object_unref (priv->view); + priv->view = NULL; + } + + if (priv->attachment != NULL) { + g_signal_handler_disconnect ( + priv->attachment, + priv->reference_handler_id); + g_object_unref (priv->attachment); + priv->attachment = NULL; + } + + if (priv->expand_button != NULL) { + g_object_unref (priv->expand_button); + priv->expand_button = NULL; + } + + if (priv->toggle_button != NULL) { + g_object_unref (priv->toggle_button); + priv->toggle_button = NULL; + } + + if (priv->cell_view != NULL) { + g_object_unref (priv->cell_view); + priv->cell_view = NULL; + } + + if (priv->popup_menu != NULL) { + g_signal_handlers_disconnect_matched ( + priv->popup_menu, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->popup_menu); + priv->popup_menu = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_attachment_button_parent_class)->dispose (object); +} + +static void +attachment_button_style_set (GtkWidget *widget, + GtkStyle *previous_style) +{ + EAttachmentButton *button; + + /* Chain up to parent's style_set() method. */ + GTK_WIDGET_CLASS (e_attachment_button_parent_class)-> + style_set (widget, previous_style); + + button = E_ATTACHMENT_BUTTON (widget); + attachment_button_update_pixbufs (button); +} + +static void +e_attachment_button_class_init (EAttachmentButtonClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EAttachmentButtonPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = attachment_button_set_property; + object_class->get_property = attachment_button_get_property; + object_class->dispose = attachment_button_dispose; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->style_set = attachment_button_style_set; + + g_object_class_install_property ( + object_class, + PROP_ATTACHMENT, + g_param_spec_object ( + "attachment", + "Attachment", + NULL, + E_TYPE_ATTACHMENT, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_EXPANDABLE, + g_param_spec_boolean ( + "expandable", + "Expandable", + NULL, + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_EXPANDED, + g_param_spec_boolean ( + "expanded", + "Expanded", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_VIEW, + g_param_spec_object ( + "view", + "View", + NULL, + E_TYPE_ATTACHMENT_VIEW, + G_PARAM_READWRITE)); +} + +static void +e_attachment_button_init (EAttachmentButton *button) +{ + GtkCellRenderer *renderer; + GtkCellLayout *cell_layout; + GtkTargetEntry *targets; + GtkTargetList *list; + GtkWidget *container; + GtkWidget *widget; + GtkStyleContext *context; + gint n_targets; + + button->priv = E_ATTACHMENT_BUTTON_GET_PRIVATE (button); + + /* Configure Widgets */ + + container = GTK_WIDGET (button); + context = gtk_widget_get_style_context (container); + gtk_style_context_add_class (context, "linked"); + + widget = gtk_button_new (); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + button->priv->expand_button = g_object_ref (widget); + gtk_widget_show (widget); + + g_object_bind_property ( + button, "expandable", + widget, "sensitive", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + widget = gtk_toggle_button_new (); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + button->priv->toggle_button = g_object_ref (widget); + gtk_widget_show (widget); + + container = button->priv->expand_button; + + widget = gtk_cell_view_new (); + gtk_container_add (GTK_CONTAINER (container), widget); + button->priv->cell_view = g_object_ref (widget); + gtk_widget_show (widget); + + container = button->priv->toggle_button; + + widget = gtk_arrow_new (GTK_ARROW_DOWN, GTK_SHADOW_NONE); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + /* Configure Renderers */ + + cell_layout = GTK_CELL_LAYOUT (button->priv->cell_view); + + renderer = gtk_cell_renderer_pixbuf_new (); + g_object_set (renderer, "is-expander", TRUE, NULL); + gtk_cell_layout_pack_start (cell_layout, renderer, FALSE); + + g_object_bind_property ( + button, "expanded", + renderer, "is-expanded", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + renderer = gtk_cell_renderer_pixbuf_new (); + g_object_set (renderer, "stock-size", GTK_ICON_SIZE_BUTTON, NULL); + gtk_cell_layout_pack_start (cell_layout, renderer, FALSE); + + gtk_cell_layout_add_attribute ( + cell_layout, renderer, "gicon", + E_ATTACHMENT_STORE_COLUMN_ICON); + + /* Configure Drag and Drop */ + + list = gtk_target_list_new (NULL, 0); + gtk_target_list_add_uri_targets (list, 0); + targets = gtk_target_table_new_from_list (list, &n_targets); + + gtk_drag_source_set ( + button->priv->expand_button, GDK_BUTTON1_MASK, + targets, n_targets, GDK_ACTION_COPY); + + gtk_drag_source_set ( + button->priv->toggle_button, GDK_BUTTON1_MASK, + targets, n_targets, GDK_ACTION_COPY); + + gtk_target_table_free (targets, n_targets); + gtk_target_list_unref (list); + + /* Configure Signal Handlers */ + + g_signal_connect_swapped ( + button->priv->expand_button, "clicked", + G_CALLBACK (attachment_button_expand_clicked_cb), button); + + g_signal_connect_swapped ( + button->priv->expand_button, "drag-begin", + G_CALLBACK (attachment_button_expand_drag_begin_cb), + button); + + g_signal_connect_swapped ( + button->priv->expand_button, "drag-data-get", + G_CALLBACK (attachment_button_expand_drag_data_get_cb), + button); + + g_signal_connect_swapped ( + button->priv->expand_button, "drag-end", + G_CALLBACK (attachment_button_expand_drag_end_cb), + button); + + g_signal_connect_swapped ( + button->priv->toggle_button, "button-press-event", + G_CALLBACK (attachment_button_toggle_button_press_event_cb), + button); + + g_signal_connect_swapped ( + button->priv->toggle_button, "drag-begin", + G_CALLBACK (attachment_button_expand_drag_begin_cb), + button); + + g_signal_connect_swapped ( + button->priv->toggle_button, "drag-data-get", + G_CALLBACK (attachment_button_expand_drag_data_get_cb), + button); + + g_signal_connect_swapped ( + button->priv->toggle_button, "drag-end", + G_CALLBACK (attachment_button_expand_drag_end_cb), + button); +} + +GtkWidget * +e_attachment_button_new () +{ + return g_object_new ( + E_TYPE_ATTACHMENT_BUTTON, NULL); +} + +EAttachmentView * +e_attachment_button_get_view (EAttachmentButton *button) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_BUTTON (button), NULL); + + return button->priv->view; +} + +void +e_attachment_button_set_view (EAttachmentButton *button, + EAttachmentView *view) +{ + GtkWidget *popup_menu; + + g_return_if_fail (button->priv->view == NULL); + + g_object_ref (view); + if (button->priv->view) + g_object_unref (button->priv->view); + button->priv->view = view; + + popup_menu = e_attachment_view_get_popup_menu (view); + + g_signal_connect_swapped ( + popup_menu, "deactivate", + G_CALLBACK (attachment_button_menu_deactivate_cb), button); + + /* Keep a reference to the popup menu so we can + * disconnect the signal handler in dispose(). */ + if (button->priv->popup_menu) + g_object_unref (button->priv->popup_menu); + button->priv->popup_menu = g_object_ref (popup_menu); +} + +EAttachment * +e_attachment_button_get_attachment (EAttachmentButton *button) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_BUTTON (button), NULL); + + return button->priv->attachment; +} + +void +e_attachment_button_set_attachment (EAttachmentButton *button, + EAttachment *attachment) +{ + GtkTargetEntry *targets; + GtkTargetList *list; + gint n_targets; + + g_return_if_fail (E_IS_ATTACHMENT_BUTTON (button)); + + if (attachment != NULL) { + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + g_object_ref (attachment); + } + + if (button->priv->attachment != NULL) { + g_object_unref (button->priv->can_show_binding); + button->priv->can_show_binding = NULL; + g_object_unref (button->priv->shown_binding); + button->priv->shown_binding = NULL; + g_signal_handler_disconnect ( + button->priv->attachment, + button->priv->reference_handler_id); + g_object_unref (button->priv->attachment); + } + + button->priv->attachment = attachment; + + if (attachment != NULL) { + GBinding *binding; + gulong handler_id; + + binding = g_object_bind_property ( + attachment, "can-show", + button, "expandable", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + button->priv->can_show_binding = binding; + + binding = g_object_bind_property ( + attachment, "shown", + button, "expanded", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + button->priv->shown_binding = binding; + + handler_id = g_signal_connect_swapped ( + attachment, "notify::reference", + G_CALLBACK (attachment_button_update_cell_view), + button); + button->priv->reference_handler_id = handler_id; + + attachment_button_update_cell_view (button); + attachment_button_update_pixbufs (button); + } + + /* update drag sources */ + list = gtk_target_list_new (NULL, 0); + gtk_target_list_add_uri_targets (list, 0); + + if (attachment) { + gchar *simple_type; + + simple_type = e_attachment_get_mime_type (attachment); + if (simple_type) { + GtkTargetEntry attach_entry[] = { { NULL, 0, 2 } }; + + attach_entry[0].target = simple_type; + + gtk_target_list_add_table ( + list, attach_entry, + G_N_ELEMENTS (attach_entry)); + + g_free (simple_type); + } + } + + targets = gtk_target_table_new_from_list (list, &n_targets); + + gtk_drag_source_set ( + button->priv->expand_button, GDK_BUTTON1_MASK, + targets, n_targets, GDK_ACTION_COPY); + + gtk_drag_source_set ( + button->priv->toggle_button, GDK_BUTTON1_MASK, + targets, n_targets, GDK_ACTION_COPY); + + gtk_target_table_free (targets, n_targets); + gtk_target_list_unref (list); + + g_object_notify (G_OBJECT (button), "attachment"); +} + +gboolean +e_attachment_button_get_expandable (EAttachmentButton *button) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_BUTTON (button), FALSE); + + return button->priv->expandable; +} + +void +e_attachment_button_set_expandable (EAttachmentButton *button, + gboolean expandable) +{ + g_return_if_fail (E_IS_ATTACHMENT_BUTTON (button)); + + if (button->priv->expandable == expandable) + return; + + button->priv->expandable = expandable; + + if (!expandable) + e_attachment_button_set_expanded (button, FALSE); + + g_object_notify (G_OBJECT (button), "expandable"); +} + +gboolean +e_attachment_button_get_expanded (EAttachmentButton *button) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_BUTTON (button), FALSE); + + return button->priv->expanded; +} + +void +e_attachment_button_set_expanded (EAttachmentButton *button, + gboolean expanded) +{ + g_return_if_fail (E_IS_ATTACHMENT_BUTTON (button)); + + if (button->priv->expanded == expanded) + return; + + button->priv->expanded = expanded; + + g_object_notify (G_OBJECT (button), "expanded"); +} diff --git a/e-util/e-attachment-button.h b/e-util/e-attachment-button.h new file mode 100644 index 0000000000..abe5fa4dc9 --- /dev/null +++ b/e-util/e-attachment-button.h @@ -0,0 +1,91 @@ +/* + * e-attachment-button.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_BUTTON_H +#define E_ATTACHMENT_BUTTON_H + +#include <gtk/gtk.h> +#include <e-util/e-attachment.h> +#include <e-util/e-attachment-view.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_BUTTON \ + (e_attachment_button_get_type ()) +#define E_ATTACHMENT_BUTTON(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_BUTTON, EAttachmentButton)) +#define E_ATTACHMENT_BUTTON_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_BUTTON, EAttachmentButtonClass)) +#define E_IS_ATTACHMENT_BUTTON(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_BUTTON)) +#define E_IS_ATTACHMENT_BUTTON_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_BUTTON)) +#define E_ATTACHMENT_BUTTON_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_BUTTON, EAttachmentButtonClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentButton EAttachmentButton; +typedef struct _EAttachmentButtonClass EAttachmentButtonClass; +typedef struct _EAttachmentButtonPrivate EAttachmentButtonPrivate; + +struct _EAttachmentButton { + GtkBox parent; + EAttachmentButtonPrivate *priv; +}; + +struct _EAttachmentButtonClass { + GtkBoxClass parent_class; +}; + +GType e_attachment_button_get_type (void); +GtkWidget * e_attachment_button_new (void); +EAttachmentView * + e_attachment_button_get_view (EAttachmentButton *button); +void e_attachment_button_set_view (EAttachmentButton *button, + EAttachmentView *view); +EAttachment * e_attachment_button_get_attachment + (EAttachmentButton *button); +void e_attachment_button_set_attachment + (EAttachmentButton *button, + EAttachment *attachment); +gboolean e_attachment_button_get_expandable + (EAttachmentButton *button); +void e_attachment_button_set_expandable + (EAttachmentButton *button, + gboolean expandable); +gboolean e_attachment_button_get_expanded + (EAttachmentButton *button); +void e_attachment_button_set_expanded + (EAttachmentButton *button, + gboolean expanded); + +G_END_DECLS + +#endif /* E_ATTACHMENT_BUTTON_H */ diff --git a/e-util/e-attachment-dialog.c b/e-util/e-attachment-dialog.c new file mode 100644 index 0000000000..9a9a1e7942 --- /dev/null +++ b/e-util/e-attachment-dialog.c @@ -0,0 +1,425 @@ +/* + * e-attachment-dialog.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-dialog.h" + +#include <glib/gi18n.h> + +#define E_ATTACHMENT_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT_DIALOG, EAttachmentDialogPrivate)) + +struct _EAttachmentDialogPrivate { + EAttachment *attachment; + GtkWidget *display_name_entry; + GtkWidget *description_entry; + GtkWidget *content_type_label; + GtkWidget *disposition_checkbox; +}; + +enum { + PROP_0, + PROP_ATTACHMENT +}; + +G_DEFINE_TYPE ( + EAttachmentDialog, + e_attachment_dialog, + GTK_TYPE_DIALOG) + +static void +attachment_dialog_update (EAttachmentDialog *dialog) +{ + EAttachment *attachment; + GFileInfo *file_info; + GtkWidget *widget; + const gchar *content_type; + const gchar *display_name; + const gchar *description; + const gchar *disposition; + gchar *type_description = NULL; + gboolean sensitive; + gboolean active; + + attachment = e_attachment_dialog_get_attachment (dialog); + + if (attachment != NULL) { + file_info = e_attachment_get_file_info (attachment); + description = e_attachment_get_description (attachment); + disposition = e_attachment_get_disposition (attachment); + } else { + file_info = NULL; + description = NULL; + disposition = NULL; + } + + if (file_info != NULL) { + content_type = g_file_info_get_content_type (file_info); + display_name = g_file_info_get_display_name (file_info); + } else { + content_type = NULL; + display_name = NULL; + } + + if (content_type != NULL) { + gchar *comment; + gchar *mime_type; + + comment = g_content_type_get_description (content_type); + mime_type = g_content_type_get_mime_type (content_type); + + type_description = + g_strdup_printf ("%s (%s)", comment, mime_type); + + g_free (comment); + g_free (mime_type); + } + + sensitive = G_IS_FILE_INFO (file_info); + + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (dialog), GTK_RESPONSE_OK, sensitive); + + widget = dialog->priv->display_name_entry; + gtk_widget_set_sensitive (widget, sensitive); + if (display_name != NULL) + gtk_entry_set_text (GTK_ENTRY (widget), display_name); + + widget = dialog->priv->description_entry; + gtk_widget_set_sensitive (widget, sensitive); + if (description != NULL) + gtk_entry_set_text (GTK_ENTRY (widget), description); + + widget = dialog->priv->content_type_label; + gtk_label_set_text (GTK_LABEL (widget), type_description); + + active = (g_strcmp0 (disposition, "inline") == 0); + widget = dialog->priv->disposition_checkbox; + gtk_widget_set_sensitive (widget, sensitive); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), active); + + g_free (type_description); +} + +static void +attachment_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ATTACHMENT: + e_attachment_dialog_set_attachment ( + E_ATTACHMENT_DIALOG (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ATTACHMENT: + g_value_set_object ( + value, e_attachment_dialog_get_attachment ( + E_ATTACHMENT_DIALOG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_dialog_dispose (GObject *object) +{ + EAttachmentDialogPrivate *priv; + + priv = E_ATTACHMENT_DIALOG_GET_PRIVATE (object); + + if (priv->attachment != NULL) { + g_object_unref (priv->attachment); + priv->attachment = NULL; + } + + if (priv->display_name_entry != NULL) { + g_object_unref (priv->display_name_entry); + priv->display_name_entry = NULL; + } + + if (priv->description_entry != NULL) { + g_object_unref (priv->description_entry); + priv->description_entry = NULL; + } + + if (priv->content_type_label != NULL) { + g_object_unref (priv->content_type_label); + priv->content_type_label = NULL; + } + + if (priv->disposition_checkbox != NULL) { + g_object_unref (priv->disposition_checkbox); + priv->disposition_checkbox = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_attachment_dialog_parent_class)->dispose (object); +} + +static void +attachment_dialog_map (GtkWidget *widget) +{ + GtkWidget *action_area; + GtkWidget *content_area; + + /* Chain up to parent's map() method. */ + GTK_WIDGET_CLASS (e_attachment_dialog_parent_class)->map (widget); + + /* XXX Override GtkDialog's broken style property defaults. */ + action_area = gtk_dialog_get_action_area (GTK_DIALOG (widget)); + content_area = gtk_dialog_get_content_area (GTK_DIALOG (widget)); + + gtk_box_set_spacing (GTK_BOX (content_area), 12); + gtk_container_set_border_width (GTK_CONTAINER (action_area), 0); + gtk_container_set_border_width (GTK_CONTAINER (content_area), 12); +} + +static void +attachment_dialog_response (GtkDialog *dialog, + gint response_id) +{ + EAttachmentDialogPrivate *priv; + EAttachment *attachment; + GtkToggleButton *button; + GFileInfo *file_info; + CamelMimePart *mime_part; + const gchar *attribute; + const gchar *text; + gboolean active; + + if (response_id != GTK_RESPONSE_OK) + return; + + priv = E_ATTACHMENT_DIALOG_GET_PRIVATE (dialog); + g_return_if_fail (E_IS_ATTACHMENT (priv->attachment)); + attachment = priv->attachment; + + file_info = e_attachment_get_file_info (attachment); + g_return_if_fail (G_IS_FILE_INFO (file_info)); + + mime_part = e_attachment_get_mime_part (attachment); + + attribute = G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME; + text = gtk_entry_get_text (GTK_ENTRY (priv->display_name_entry)); + g_file_info_set_attribute_string (file_info, attribute, text); + + if (mime_part != NULL) + camel_mime_part_set_filename (mime_part, text); + + attribute = G_FILE_ATTRIBUTE_STANDARD_DESCRIPTION; + text = gtk_entry_get_text (GTK_ENTRY (priv->description_entry)); + g_file_info_set_attribute_string (file_info, attribute, text); + + if (mime_part != NULL) + camel_mime_part_set_description (mime_part, text); + + button = GTK_TOGGLE_BUTTON (priv->disposition_checkbox); + active = gtk_toggle_button_get_active (button); + text = active ? "inline" : "attachment"; + e_attachment_set_disposition (attachment, text); + + if (mime_part != NULL) + camel_mime_part_set_disposition (mime_part, text); + + g_object_notify (G_OBJECT (attachment), "file-info"); +} + +static void +e_attachment_dialog_class_init (EAttachmentDialogClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkDialogClass *dialog_class; + + g_type_class_add_private (class, sizeof (EAttachmentDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = attachment_dialog_set_property; + object_class->get_property = attachment_dialog_get_property; + object_class->dispose = attachment_dialog_dispose; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->map = attachment_dialog_map; + + dialog_class = GTK_DIALOG_CLASS (class); + dialog_class->response = attachment_dialog_response; + + g_object_class_install_property ( + object_class, + PROP_ATTACHMENT, + g_param_spec_object ( + "attachment", + "Attachment", + NULL, + E_TYPE_ATTACHMENT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); +} + +static void +e_attachment_dialog_init (EAttachmentDialog *dialog) +{ + GtkWidget *container; + GtkWidget *widget; + + dialog->priv = E_ATTACHMENT_DIALOG_GET_PRIVATE (dialog); + + gtk_dialog_add_button ( + GTK_DIALOG (dialog), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL); + gtk_dialog_add_button ( + GTK_DIALOG (dialog), GTK_STOCK_OK, GTK_RESPONSE_OK); + gtk_window_set_icon_name ( + GTK_WINDOW (dialog), "mail-attachment"); + gtk_window_set_title ( + GTK_WINDOW (dialog), _("Attachment Properties")); + + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + + container = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + + widget = gtk_table_new (4, 2, FALSE); + gtk_table_set_col_spacings (GTK_TABLE (widget), 6); + gtk_table_set_row_spacings (GTK_TABLE (widget), 6); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_entry_new (); + gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE); + gtk_table_attach ( + GTK_TABLE (container), widget, + 1, 2, 0, 1, GTK_FILL | GTK_EXPAND, 0, 0, 0); + dialog->priv->display_name_entry = g_object_ref (widget); + gtk_widget_show (widget); + + widget = gtk_label_new_with_mnemonic (_("F_ilename:")); + gtk_misc_set_alignment (GTK_MISC (widget), 1.0, 0.5); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->display_name_entry); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, 0, 1, GTK_FILL, 0, 0, 0); + gtk_widget_show (widget); + + widget = gtk_entry_new (); + gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE); + gtk_table_attach ( + GTK_TABLE (container), widget, + 1, 2, 1, 2, GTK_FILL | GTK_EXPAND, 0, 0, 0); + dialog->priv->description_entry = g_object_ref (widget); + gtk_widget_show (widget); + + widget = gtk_label_new_with_mnemonic (_("_Description:")); + gtk_misc_set_alignment (GTK_MISC (widget), 1.0, 0.5); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->description_entry); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, 1, 2, GTK_FILL, 0, 0, 0); + gtk_widget_show (widget); + + widget = gtk_label_new (NULL); + gtk_label_set_selectable (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_table_attach ( + GTK_TABLE (container), widget, + 1, 2, 2, 3, GTK_FILL | GTK_EXPAND, 0, 0, 0); + dialog->priv->content_type_label = g_object_ref (widget); + gtk_widget_show (widget); + + widget = gtk_label_new (_("MIME Type:")); + gtk_misc_set_alignment (GTK_MISC (widget), 1.0, 0.5); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, 2, 3, GTK_FILL, 0, 0, 0); + gtk_widget_show (widget); + + widget = gtk_check_button_new_with_mnemonic ( + _("_Suggest automatic display of attachment")); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 2, 3, 4, GTK_FILL | GTK_EXPAND, 0, 0, 0); + dialog->priv->disposition_checkbox = g_object_ref (widget); + gtk_widget_show (widget); +} + +GtkWidget * +e_attachment_dialog_new (GtkWindow *parent, + EAttachment *attachment) +{ + if (parent != NULL) + g_return_val_if_fail (GTK_IS_WINDOW (parent), NULL); + if (attachment != NULL) + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + return g_object_new ( + E_TYPE_ATTACHMENT_DIALOG, + "transient-for", parent, "attachment", attachment, NULL); +} + +EAttachment * +e_attachment_dialog_get_attachment (EAttachmentDialog *dialog) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_DIALOG (dialog), NULL); + + return dialog->priv->attachment; +} + +void +e_attachment_dialog_set_attachment (EAttachmentDialog *dialog, + EAttachment *attachment) +{ + g_return_if_fail (E_IS_ATTACHMENT_DIALOG (dialog)); + + if (attachment != NULL) { + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + g_object_ref (attachment); + } + + if (dialog->priv->attachment != NULL) + g_object_unref (dialog->priv->attachment); + + dialog->priv->attachment = attachment; + + attachment_dialog_update (dialog); + + g_object_notify (G_OBJECT (dialog), "attachment"); +} diff --git a/e-util/e-attachment-dialog.h b/e-util/e-attachment-dialog.h new file mode 100644 index 0000000000..af7141190e --- /dev/null +++ b/e-util/e-attachment-dialog.h @@ -0,0 +1,77 @@ +/* + * e-attachment-dialog.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_DIALOG_H +#define E_ATTACHMENT_DIALOG_H + +#include <gtk/gtk.h> +#include <e-util/e-attachment.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_DIALOG \ + (e_attachment_dialog_get_type ()) +#define E_ATTACHMENT_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_DIALOG, EAttachmentDialog)) +#define E_ATTACHMENT_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_DIALOG, EAttachmentDialogClass)) +#define E_IS_ATTACHMENT_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_DIALOG)) +#define E_IS_ATTACHMENT_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_DIALOG)) +#define E_ATTACHMENT_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_DIALOG, EAttachmentDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentDialog EAttachmentDialog; +typedef struct _EAttachmentDialogClass EAttachmentDialogClass; +typedef struct _EAttachmentDialogPrivate EAttachmentDialogPrivate; + +struct _EAttachmentDialog { + GtkDialog parent; + EAttachmentDialogPrivate *priv; +}; + +struct _EAttachmentDialogClass { + GtkDialogClass parent_class; +}; + +GType e_attachment_dialog_get_type (void); +GtkWidget * e_attachment_dialog_new (GtkWindow *parent, + EAttachment *attachment); +EAttachment * e_attachment_dialog_get_attachment + (EAttachmentDialog *dialog); +void e_attachment_dialog_set_attachment + (EAttachmentDialog *dialog, + EAttachment *attachment); + +G_END_DECLS + +#endif /* E_ATTACHMENT_DIALOG_H */ diff --git a/e-util/e-attachment-handler-image.c b/e-util/e-attachment-handler-image.c new file mode 100644 index 0000000000..36c3a83614 --- /dev/null +++ b/e-util/e-attachment-handler-image.c @@ -0,0 +1,246 @@ +/* + * e-attachment-handler-image.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-handler-image.h" + +#include <glib/gi18n.h> +#include <gdesktop-enums.h> + +#define E_ATTACHMENT_HANDLER_IMAGE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT_HANDLER_IMAGE, EAttachmentHandlerImagePrivate)) + +struct _EAttachmentHandlerImagePrivate { + gint placeholder; +}; + +static const gchar *ui = +"<ui>" +" <popup name='context'>" +" <placeholder name='custom-actions'>" +" <menuitem action='image-set-as-background'/>" +" </placeholder>" +" </popup>" +"</ui>"; + +G_DEFINE_TYPE ( + EAttachmentHandlerImage, + e_attachment_handler_image, + E_TYPE_ATTACHMENT_HANDLER) + +static void +action_image_set_as_background_saved_cb (EAttachment *attachment, + GAsyncResult *result, + EAttachmentHandler *handler) +{ + GDesktopBackgroundStyle style; + EAttachmentView *view; + GSettings *settings; + GtkWidget *dialog; + GFile *file; + gpointer parent; + gchar *uri; + GError *error = NULL; + + view = e_attachment_handler_get_view (handler); + settings = g_settings_new ("org.gnome.desktop.background"); + + file = e_attachment_save_finish (attachment, result, &error); + + if (error != NULL) + goto error; + + uri = g_file_get_uri (file); + g_settings_set_string (settings, "picture-uri", uri); + g_free (uri); + + style = g_settings_get_enum (settings, "picture-options"); + if (style == G_DESKTOP_BACKGROUND_STYLE_NONE) + g_settings_set_enum ( + settings, "picture-options", + G_DESKTOP_BACKGROUND_STYLE_WALLPAPER); + + g_object_unref (file); + + goto exit; + +error: + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + dialog = gtk_message_dialog_new_with_markup ( + parent, GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "<big><b>%s</b></big>", + _("Could not set as background")); + + gtk_message_dialog_format_secondary_text ( + GTK_MESSAGE_DIALOG (dialog), "%s", error->message); + + gtk_dialog_run (GTK_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + g_error_free (error); + +exit: + g_object_unref (settings); + g_object_unref (handler); +} + +static void +action_image_set_as_background_cb (GtkAction *action, + EAttachmentHandler *handler) +{ + EAttachmentView *view; + EAttachment *attachment; + GFile *destination; + GList *selected; + const gchar *path; + + view = e_attachment_handler_get_view (handler); + selected = e_attachment_view_get_selected_attachments (view); + g_return_if_fail (g_list_length (selected) == 1); + attachment = E_ATTACHMENT (selected->data); + + /* Save the image under the user's Pictures directory. */ + path = g_get_user_special_dir (G_USER_DIRECTORY_PICTURES); + destination = g_file_new_for_path (path); + g_mkdir_with_parents (path, 0755); + + e_attachment_save_async ( + attachment, destination, (GAsyncReadyCallback) + action_image_set_as_background_saved_cb, + g_object_ref (handler)); + + g_object_unref (destination); + + g_list_foreach (selected, (GFunc) g_object_unref, NULL); + g_list_free (selected); +} + +static GtkActionEntry standard_entries[] = { + + { "image-set-as-background", + NULL, + N_("Set as _Background"), + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_image_set_as_background_cb) } +}; + +static void +attachment_handler_image_update_actions_cb (EAttachmentView *view, + EAttachmentHandler *handler) +{ + EAttachment *attachment; + GFileInfo *file_info; + GtkActionGroup *action_group; + const gchar *content_type; + gchar *mime_type; + GList *selected; + gboolean visible = FALSE; + + selected = e_attachment_view_get_selected_attachments (view); + + if (g_list_length (selected) != 1) + goto exit; + + attachment = E_ATTACHMENT (selected->data); + file_info = e_attachment_get_file_info (attachment); + + if (file_info == NULL) + goto exit; + + if (e_attachment_get_loading (attachment)) + goto exit; + + if (e_attachment_get_saving (attachment)) + goto exit; + + content_type = g_file_info_get_content_type (file_info); + + mime_type = g_content_type_get_mime_type (content_type); + visible = (g_ascii_strncasecmp (mime_type, "image/", 6) == 0); + g_free (mime_type); + +exit: + action_group = e_attachment_view_get_action_group (view, "image"); + gtk_action_group_set_visible (action_group, visible); + + g_list_foreach (selected, (GFunc) g_object_unref, NULL); + g_list_free (selected); +} + +static void +attachment_handler_image_constructed (GObject *object) +{ + EAttachmentHandler *handler; + EAttachmentView *view; + GtkActionGroup *action_group; + GtkUIManager *ui_manager; + GError *error = NULL; + + handler = E_ATTACHMENT_HANDLER (object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_attachment_handler_image_parent_class)->constructed (object); + + view = e_attachment_handler_get_view (handler); + + action_group = e_attachment_view_add_action_group (view, "image"); + gtk_action_group_add_actions ( + action_group, standard_entries, + G_N_ELEMENTS (standard_entries), object); + + ui_manager = e_attachment_view_get_ui_manager (view); + gtk_ui_manager_add_ui_from_string (ui_manager, ui, -1, &error); + + if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_signal_connect ( + view, "update-actions", + G_CALLBACK (attachment_handler_image_update_actions_cb), + object); +} + +static void +e_attachment_handler_image_class_init (EAttachmentHandlerImageClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EAttachmentHandlerImagePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->constructed = attachment_handler_image_constructed; +} + +static void +e_attachment_handler_image_init (EAttachmentHandlerImage *handler) +{ + handler->priv = E_ATTACHMENT_HANDLER_IMAGE_GET_PRIVATE (handler); +} diff --git a/e-util/e-attachment-handler-image.h b/e-util/e-attachment-handler-image.h new file mode 100644 index 0000000000..e0e0cb3b23 --- /dev/null +++ b/e-util/e-attachment-handler-image.h @@ -0,0 +1,69 @@ +/* + * e-attachment-handler-image.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_HANDLER_IMAGE_H +#define E_ATTACHMENT_HANDLER_IMAGE_H + +#include <e-util/e-attachment-handler.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_HANDLER_IMAGE \ + (e_attachment_handler_image_get_type ()) +#define E_ATTACHMENT_HANDLER_IMAGE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_HANDLER_IMAGE, EAttachmentHandlerImage)) +#define E_ATTACHMENT_HANDLER_IMAGE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_HANDLER_IMAGE, EAttachmentHandlerImageClass)) +#define E_IS_ATTACHMENT_HANDLER_IMAGE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_HANDLER_IMAGE)) +#define E_IS_ATTACHMENT_HANDLER_IMAGE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_HANDLER_IMAGE)) +#define E_ATTACHMENT_HANDLER_IMAGE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_HANDLER_IMAGE, EAttachmentHandlerImageClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentHandlerImage EAttachmentHandlerImage; +typedef struct _EAttachmentHandlerImageClass EAttachmentHandlerImageClass; +typedef struct _EAttachmentHandlerImagePrivate EAttachmentHandlerImagePrivate; + +struct _EAttachmentHandlerImage { + EAttachmentHandler parent; + EAttachmentHandlerImagePrivate *priv; +}; + +struct _EAttachmentHandlerImageClass { + EAttachmentHandlerClass parent_class; +}; + +GType e_attachment_handler_image_get_type (void); + +G_END_DECLS + +#endif /* E_ATTACHMENT_HANDLER_IMAGE_H */ diff --git a/e-util/e-attachment-handler-sendto.c b/e-util/e-attachment-handler-sendto.c new file mode 100644 index 0000000000..f0fe698713 --- /dev/null +++ b/e-util/e-attachment-handler-sendto.c @@ -0,0 +1,229 @@ +/* + * e-attachment-handler-sendto.c + * + * Copyright (C) 2009 Matthew Barnes + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-handler-sendto.h" + +#include <errno.h> + +#include <glib/gi18n-lib.h> + +static const gchar *ui = +"<ui>" +" <popup name='context'>" +" <placeholder name='custom-actions'>" +" <menuitem action='sendto'/>" +" </placeholder>" +" </popup>" +"</ui>"; + +G_DEFINE_TYPE ( + EAttachmentHandlerSendto, + e_attachment_handler_sendto, + E_TYPE_ATTACHMENT_HANDLER) + +static void +sendto_save_finished_cb (EAttachment *attachment, + GAsyncResult *result, + EAttachmentHandler *handler) +{ + EAttachmentView *view; + EAttachmentStore *store; + GtkWidget *dialog; + gchar **uris; + gpointer parent; + gchar *arguments; + gchar *command_line; + guint n_uris = 1; + GError *error = NULL; + + view = e_attachment_handler_get_view (handler); + store = e_attachment_view_get_store (view); + + uris = e_attachment_store_get_uris_finish (store, result, &error); + + if (uris != NULL) + n_uris = g_strv_length (uris); + + if (error != NULL) + goto error; + + arguments = g_strjoinv (" ", uris); + command_line = g_strdup_printf ("nautilus-sendto %s", arguments); + + g_message ("Command: %s", command_line); + g_spawn_command_line_async (command_line, &error); + + g_free (command_line); + g_free (arguments); + + if (error != NULL) + goto error; + + goto exit; + +error: + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + dialog = gtk_message_dialog_new_with_markup ( + parent, GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "<big><b>%s</b></big>", + ngettext ("Could not send attachment", + "Could not send attachments", n_uris)); + + gtk_message_dialog_format_secondary_text ( + GTK_MESSAGE_DIALOG (dialog), "%s", error->message); + + gtk_dialog_run (GTK_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + g_error_free (error); + +exit: + g_object_unref (handler); + g_strfreev (uris); +} + +static void +action_sendto_cb (GtkAction *action, + EAttachmentHandler *handler) +{ + EAttachmentView *view; + EAttachmentStore *store; + GList *selected; + + view = e_attachment_handler_get_view (handler); + store = e_attachment_view_get_store (view); + + selected = e_attachment_view_get_selected_attachments (view); + g_return_if_fail (selected != NULL); + + e_attachment_store_get_uris_async ( + store, selected, (GAsyncReadyCallback) + sendto_save_finished_cb, g_object_ref (handler)); + + g_list_foreach (selected, (GFunc) g_object_unref, NULL); + g_list_free (selected); +} + +static GtkActionEntry standard_entries[] = { + + { "sendto", + "document-send", + N_("_Send To..."), + NULL, + N_("Send the selected attachments somewhere"), + G_CALLBACK (action_sendto_cb) } +}; + +static void +attachment_handler_sendto_update_actions_cb (EAttachmentView *view, + EAttachmentHandler *handler) +{ + GtkActionGroup *action_group; + GList *selected, *iter; + gboolean visible = FALSE; + gchar *program; + + program = g_find_program_in_path ("nautilus-sendto"); + selected = e_attachment_view_get_selected_attachments (view); + + if (program == NULL || selected == NULL) + goto exit; + + /* Make sure no file transfers are in progress. */ + for (iter = selected; iter != NULL; iter = iter->next) { + EAttachment *attachment = iter->data; + + if (e_attachment_get_loading (attachment)) + goto exit; + + if (e_attachment_get_saving (attachment)) + goto exit; + } + + visible = TRUE; + +exit: + action_group = e_attachment_view_get_action_group (view, "sendto"); + gtk_action_group_set_visible (action_group, visible); + + g_list_foreach (selected, (GFunc) g_object_unref, NULL); + g_list_free (selected); + + g_free (program); +} + +static void +attachment_handler_sendto_constructed (GObject *object) +{ + EAttachmentHandler *handler; + EAttachmentView *view; + GtkActionGroup *action_group; + GtkUIManager *ui_manager; + GError *error = NULL; + + handler = E_ATTACHMENT_HANDLER (object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_attachment_handler_sendto_parent_class)->constructed (object); + + view = e_attachment_handler_get_view (handler); + ui_manager = e_attachment_view_get_ui_manager (view); + + action_group = gtk_action_group_new ("sendto"); + gtk_action_group_set_translation_domain ( + action_group, GETTEXT_PACKAGE); + gtk_action_group_add_actions ( + action_group, standard_entries, + G_N_ELEMENTS (standard_entries), object); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + + gtk_ui_manager_add_ui_from_string (ui_manager, ui, -1, &error); + + if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_signal_connect ( + view, "update-actions", + G_CALLBACK (attachment_handler_sendto_update_actions_cb), + object); +} + +static void +e_attachment_handler_sendto_class_init (EAttachmentHandlerSendtoClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + object_class->constructed = attachment_handler_sendto_constructed; +} + +static void +e_attachment_handler_sendto_init (EAttachmentHandlerSendto *handler) +{ +} diff --git a/e-util/e-attachment-handler-sendto.h b/e-util/e-attachment-handler-sendto.h new file mode 100644 index 0000000000..17115c4104 --- /dev/null +++ b/e-util/e-attachment-handler-sendto.h @@ -0,0 +1,66 @@ +/* + * e-attachment-handler-sendto.h + * + * Copyright (C) 2009 Matthew Barnes + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_HANDLER_SENDTO_H +#define E_ATTACHMENT_HANDLER_SENDTO_H + +#include <e-util/e-attachment-handler.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_HANDLER_SENDTO \ + (e_attachment_handler_sendto_get_type ()) +#define E_ATTACHMENT_HANDLER_SENDTO(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_HANDLER_SENDTO, EAttachmentHandlerSendto)) +#define E_ATTACHMENT_HANDLER_SENDTO_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_HANDLER_SENDTO, EAttachmentHandlerSendtoClass)) +#define E_IS_ATTACHMENT_HANDLER_SENDTO(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_HANDLER_SENDTO)) +#define E_IS_ATTACHMENT_HANDLER_SENDTO_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_HANDLER_SENDTO)) +#define E_ATTACHMENT_HANDLER_SENDTO_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_HANDLER_SENDTO, EAttachmentHandlerSendtoClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentHandlerSendto EAttachmentHandlerSendto; +typedef struct _EAttachmentHandlerSendtoClass EAttachmentHandlerSendtoClass; + +struct _EAttachmentHandlerSendto { + EAttachmentHandler parent; +}; + +struct _EAttachmentHandlerSendtoClass { + EAttachmentHandlerClass parent_class; +}; + +GType e_attachment_handler_sendto_get_type (void); + +G_END_DECLS + +#endif /* E_ATTACHMENT_HANDLER_SENDTO_H */ diff --git a/e-util/e-attachment-handler.c b/e-util/e-attachment-handler.c new file mode 100644 index 0000000000..87b9abddb5 --- /dev/null +++ b/e-util/e-attachment-handler.c @@ -0,0 +1,133 @@ +/* + * e-attachment-handler.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-handler.h" + +#define E_ATTACHMENT_HANDLER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT_HANDLER, EAttachmentHandlerPrivate)) + +struct _EAttachmentHandlerPrivate { + gpointer placeholder; +}; + +G_DEFINE_TYPE ( + EAttachmentHandler, + e_attachment_handler, + E_TYPE_EXTENSION) + +static void +attachment_handler_constructed (GObject *object) +{ + EAttachmentView *view; + EAttachmentHandler *handler; + GdkDragAction drag_actions; + GtkTargetList *target_list; + const GtkTargetEntry *targets; + guint n_targets; + + handler = E_ATTACHMENT_HANDLER (object); + drag_actions = e_attachment_handler_get_drag_actions (handler); + targets = e_attachment_handler_get_target_table (handler, &n_targets); + + view = e_attachment_handler_get_view (handler); + + target_list = e_attachment_view_get_target_list (view); + gtk_target_list_add_table (target_list, targets, n_targets); + + e_attachment_view_add_drag_actions (view, drag_actions); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_attachment_handler_parent_class)->constructed (object); +} + +static void +e_attachment_handler_class_init (EAttachmentHandlerClass *class) +{ + GObjectClass *object_class; + EExtensionClass *extension_class; + + g_type_class_add_private (class, sizeof (EAttachmentHandlerPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->constructed = attachment_handler_constructed; + + extension_class = E_EXTENSION_CLASS (class); + extension_class->extensible_type = E_TYPE_ATTACHMENT_VIEW; +} + +static void +e_attachment_handler_init (EAttachmentHandler *handler) +{ + handler->priv = E_ATTACHMENT_HANDLER_GET_PRIVATE (handler); +} + +EAttachmentView * +e_attachment_handler_get_view (EAttachmentHandler *handler) +{ + EExtensible *extensible; + + /* This is purely a convenience function. */ + + g_return_val_if_fail (E_IS_ATTACHMENT_HANDLER (handler), NULL); + + extensible = e_extension_get_extensible (E_EXTENSION (handler)); + + return E_ATTACHMENT_VIEW (extensible); +} + +GdkDragAction +e_attachment_handler_get_drag_actions (EAttachmentHandler *handler) +{ + EAttachmentHandlerClass *class; + + g_return_val_if_fail (E_IS_ATTACHMENT_HANDLER (handler), 0); + + class = E_ATTACHMENT_HANDLER_GET_CLASS (handler); + + if (class->get_drag_actions != NULL) + return class->get_drag_actions (handler); + + return 0; +} + +const GtkTargetEntry * +e_attachment_handler_get_target_table (EAttachmentHandler *handler, + guint *n_targets) +{ + EAttachmentHandlerClass *class; + + g_return_val_if_fail (E_IS_ATTACHMENT_HANDLER (handler), NULL); + + class = E_ATTACHMENT_HANDLER_GET_CLASS (handler); + + if (class->get_target_table != NULL) + return class->get_target_table (handler, n_targets); + + if (n_targets != NULL) + *n_targets = 0; + + return NULL; +} diff --git a/e-util/e-attachment-handler.h b/e-util/e-attachment-handler.h new file mode 100644 index 0000000000..086ba8ff6a --- /dev/null +++ b/e-util/e-attachment-handler.h @@ -0,0 +1,84 @@ +/* + * e-attachment-handler.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_HANDLER_H +#define E_ATTACHMENT_HANDLER_H + +#include <libebackend/libebackend.h> + +#include <e-util/e-attachment-view.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_HANDLER \ + (e_attachment_handler_get_type ()) +#define E_ATTACHMENT_HANDLER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_HANDLER, EAttachmentHandler)) +#define E_ATTACHMENT_HANDLER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_HANDLER, EAttachmentHandlerClass)) +#define E_IS_ATTACHMENT_HANDLER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_HANDLER)) +#define E_IS_ATTACHMENT_HANDLER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_HANDLER)) +#define E_ATTACHMENT_HANDLER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_HANDLER, EAttachmentHandlerClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentHandler EAttachmentHandler; +typedef struct _EAttachmentHandlerClass EAttachmentHandlerClass; +typedef struct _EAttachmentHandlerPrivate EAttachmentHandlerPrivate; + +struct _EAttachmentHandler { + EExtension parent; + EAttachmentHandlerPrivate *priv; +}; + +struct _EAttachmentHandlerClass { + EExtensionClass parent_class; + + GdkDragAction (*get_drag_actions) (EAttachmentHandler *handler); + const GtkTargetEntry * + (*get_target_table) (EAttachmentHandler *handler, + guint *n_targets); +}; + +GType e_attachment_handler_get_type (void); +EAttachmentView * + e_attachment_handler_get_view (EAttachmentHandler *handler); +GdkDragAction e_attachment_handler_get_drag_actions + (EAttachmentHandler *handler); +const GtkTargetEntry * + e_attachment_handler_get_target_table + (EAttachmentHandler *handler, + guint *n_targets); + +G_END_DECLS + +#endif /* E_ATTACHMENT_HANDLER_H */ diff --git a/e-util/e-attachment-icon-view.c b/e-util/e-attachment-icon-view.c new file mode 100644 index 0000000000..2be8009e8a --- /dev/null +++ b/e-util/e-attachment-icon-view.c @@ -0,0 +1,570 @@ +/* + * e-attachment-icon-view.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-icon-view.h" + +#include <glib/gi18n.h> +#include <libebackend/libebackend.h> + +#include "e-attachment.h" +#include "e-attachment-store.h" +#include "e-attachment-view.h" + +#define E_ATTACHMENT_ICON_VIEW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT_ICON_VIEW, EAttachmentIconViewPrivate)) + +struct _EAttachmentIconViewPrivate { + EAttachmentViewPrivate view_priv; +}; + +enum { + PROP_0, + PROP_DRAGGING, + PROP_EDITABLE +}; + +static gint icon_size = GTK_ICON_SIZE_DIALOG; + +/* Forward Declarations */ +static void e_attachment_icon_view_interface_init + (EAttachmentViewInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EAttachmentIconView, + e_attachment_icon_view, + GTK_TYPE_ICON_VIEW, + G_IMPLEMENT_INTERFACE ( + E_TYPE_ATTACHMENT_VIEW, + e_attachment_icon_view_interface_init) + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +void +e_attachment_icon_view_set_default_icon_size (gint size) +{ + icon_size = size; +} + +static void +attachment_icon_view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_DRAGGING: + e_attachment_view_set_dragging ( + E_ATTACHMENT_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_EDITABLE: + e_attachment_view_set_editable ( + E_ATTACHMENT_VIEW (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_icon_view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_DRAGGING: + g_value_set_boolean ( + value, e_attachment_view_get_dragging ( + E_ATTACHMENT_VIEW (object))); + return; + + case PROP_EDITABLE: + g_value_set_boolean ( + value, e_attachment_view_get_editable ( + E_ATTACHMENT_VIEW (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_icon_view_dispose (GObject *object) +{ + e_attachment_view_dispose (E_ATTACHMENT_VIEW (object)); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_attachment_icon_view_parent_class)->dispose (object); +} + +static void +attachment_icon_view_finalize (GObject *object) +{ + e_attachment_view_finalize (E_ATTACHMENT_VIEW (object)); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_attachment_icon_view_parent_class)->finalize (object); +} + +static void +attachment_icon_view_constructed (GObject *object) +{ + GtkCellLayout *cell_layout; + GtkCellRenderer *renderer; + + cell_layout = GTK_CELL_LAYOUT (object); + + /* This needs to happen after constructor properties are set + * so that GtkCellLayout.get_area() returns something valid. */ + + renderer = gtk_cell_renderer_pixbuf_new (); + g_object_set (renderer, "stock-size", icon_size, NULL); + gtk_cell_layout_pack_start (cell_layout, renderer, FALSE); + + gtk_cell_layout_add_attribute ( + cell_layout, renderer, "gicon", + E_ATTACHMENT_STORE_COLUMN_ICON); + + renderer = gtk_cell_renderer_text_new (); + g_object_set ( + renderer, "alignment", PANGO_ALIGN_CENTER, + "wrap-mode", PANGO_WRAP_WORD, "wrap-width", 150, + "yalign", 0.0, NULL); + gtk_cell_layout_pack_start (cell_layout, renderer, FALSE); + + gtk_cell_layout_add_attribute ( + cell_layout, renderer, "text", + E_ATTACHMENT_STORE_COLUMN_CAPTION); + + renderer = gtk_cell_renderer_progress_new (); + g_object_set (renderer, "text", _("Loading"), NULL); + gtk_cell_layout_pack_start (cell_layout, renderer, TRUE); + + gtk_cell_layout_add_attribute ( + cell_layout, renderer, "value", + E_ATTACHMENT_STORE_COLUMN_PERCENT); + + gtk_cell_layout_add_attribute ( + cell_layout, renderer, "visible", + E_ATTACHMENT_STORE_COLUMN_LOADING); + + renderer = gtk_cell_renderer_progress_new (); + g_object_set (renderer, "text", _("Saving"), NULL); + gtk_cell_layout_pack_start (cell_layout, renderer, TRUE); + + gtk_cell_layout_add_attribute ( + cell_layout, renderer, "value", + E_ATTACHMENT_STORE_COLUMN_PERCENT); + + gtk_cell_layout_add_attribute ( + cell_layout, renderer, "visible", + E_ATTACHMENT_STORE_COLUMN_SAVING); + + e_extensible_load_extensions (E_EXTENSIBLE (object)); +} + +static gboolean +attachment_icon_view_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (e_attachment_view_button_press_event (view, event)) + return TRUE; + + /* Chain up to parent's button_press_event() method. */ + return GTK_WIDGET_CLASS (e_attachment_icon_view_parent_class)-> + button_press_event (widget, event); +} + +static gboolean +attachment_icon_view_button_release_event (GtkWidget *widget, + GdkEventButton *event) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (e_attachment_view_button_release_event (view, event)) + return TRUE; + + /* Chain up to parent's button_release_event() method. */ + return GTK_WIDGET_CLASS (e_attachment_icon_view_parent_class)-> + button_release_event (widget, event); +} + +static gboolean +attachment_icon_view_motion_notify_event (GtkWidget *widget, + GdkEventMotion *event) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (e_attachment_view_motion_notify_event (view, event)) + return TRUE; + + /* Chain up to parent's motion_notify_event() method. */ + return GTK_WIDGET_CLASS (e_attachment_icon_view_parent_class)-> + motion_notify_event (widget, event); +} + +static gboolean +attachment_icon_view_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (e_attachment_view_key_press_event (view, event)) + return TRUE; + + /* Chain up to parent's key_press_event() method. */ + return GTK_WIDGET_CLASS (e_attachment_icon_view_parent_class)-> + key_press_event (widget, event); +} + +static void +attachment_icon_view_drag_begin (GtkWidget *widget, + GdkDragContext *context) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + /* Chain up to parent's drag_begin() method. */ + GTK_WIDGET_CLASS (e_attachment_icon_view_parent_class)-> + drag_begin (widget, context); + + e_attachment_view_drag_begin (view, context); +} + +static void +attachment_icon_view_drag_end (GtkWidget *widget, + GdkDragContext *context) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + /* Chain up to parent's drag_end() method. */ + GTK_WIDGET_CLASS (e_attachment_icon_view_parent_class)-> + drag_end (widget, context); + + e_attachment_view_drag_end (view, context); +} + +static void +attachment_icon_view_drag_data_get (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection, + guint info, + guint time) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + e_attachment_view_drag_data_get ( + view, context, selection, info, time); +} + +static gboolean +attachment_icon_view_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + return e_attachment_view_drag_motion (view, context, x, y, time); +} + +static gboolean +attachment_icon_view_drag_drop (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (!e_attachment_view_drag_drop (view, context, x, y, time)) + return FALSE; + + /* Chain up to parent's drag_drop() method. */ + return GTK_WIDGET_CLASS (e_attachment_icon_view_parent_class)-> + drag_drop (widget, context, x, y, time); +} + +static void +attachment_icon_view_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection, + guint info, + guint time) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + e_attachment_view_drag_data_received ( + view, context, x, y, selection, info, time); +} + +static gboolean +attachment_icon_view_popup_menu (GtkWidget *widget) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + e_attachment_view_show_popup_menu (view, NULL, NULL, NULL); + + return TRUE; +} + +static void +attachment_icon_view_item_activated (GtkIconView *icon_view, + GtkTreePath *path) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (icon_view); + + e_attachment_view_open_path (view, path, NULL); +} + +static EAttachmentViewPrivate * +attachment_icon_view_get_private (EAttachmentView *view) +{ + EAttachmentIconViewPrivate *priv; + + priv = E_ATTACHMENT_ICON_VIEW_GET_PRIVATE (view); + + return &priv->view_priv; +} + +static EAttachmentStore * +attachment_icon_view_get_store (EAttachmentView *view) +{ + GtkIconView *icon_view; + GtkTreeModel *model; + + icon_view = GTK_ICON_VIEW (view); + model = gtk_icon_view_get_model (icon_view); + + return E_ATTACHMENT_STORE (model); +} + +static GtkTreePath * +attachment_icon_view_get_path_at_pos (EAttachmentView *view, + gint x, + gint y) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + return gtk_icon_view_get_path_at_pos (icon_view, x, y); +} + +static GList * +attachment_icon_view_get_selected_paths (EAttachmentView *view) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + return gtk_icon_view_get_selected_items (icon_view); +} + +static gboolean +attachment_icon_view_path_is_selected (EAttachmentView *view, + GtkTreePath *path) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + return gtk_icon_view_path_is_selected (icon_view, path); +} + +static void +attachment_icon_view_select_path (EAttachmentView *view, + GtkTreePath *path) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + gtk_icon_view_select_path (icon_view, path); +} + +static void +attachment_icon_view_unselect_path (EAttachmentView *view, + GtkTreePath *path) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + gtk_icon_view_unselect_path (icon_view, path); +} + +static void +attachment_icon_view_select_all (EAttachmentView *view) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + gtk_icon_view_select_all (icon_view); +} + +static void +attachment_icon_view_unselect_all (EAttachmentView *view) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + gtk_icon_view_unselect_all (icon_view); +} + +static void +attachment_icon_view_drag_source_set (EAttachmentView *view, + GdkModifierType start_button_mask, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + gtk_icon_view_enable_model_drag_source ( + icon_view, start_button_mask, targets, n_targets, actions); +} + +static void +attachment_icon_view_drag_dest_set (EAttachmentView *view, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + gtk_icon_view_enable_model_drag_dest ( + icon_view, targets, n_targets, actions); +} + +static void +attachment_icon_view_drag_source_unset (EAttachmentView *view) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + gtk_icon_view_unset_model_drag_source (icon_view); +} + +static void +attachment_icon_view_drag_dest_unset (EAttachmentView *view) +{ + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (view); + + gtk_icon_view_unset_model_drag_dest (icon_view); +} + +static void +e_attachment_icon_view_class_init (EAttachmentIconViewClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkIconViewClass *icon_view_class; + + g_type_class_add_private (class, sizeof (EAttachmentViewPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = attachment_icon_view_set_property; + object_class->get_property = attachment_icon_view_get_property; + object_class->dispose = attachment_icon_view_dispose; + object_class->finalize = attachment_icon_view_finalize; + object_class->constructed = attachment_icon_view_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->button_press_event = attachment_icon_view_button_press_event; + widget_class->button_release_event = attachment_icon_view_button_release_event; + widget_class->motion_notify_event = attachment_icon_view_motion_notify_event; + widget_class->key_press_event = attachment_icon_view_key_press_event; + widget_class->drag_begin = attachment_icon_view_drag_begin; + widget_class->drag_end = attachment_icon_view_drag_end; + widget_class->drag_data_get = attachment_icon_view_drag_data_get; + widget_class->drag_motion = attachment_icon_view_drag_motion; + widget_class->drag_drop = attachment_icon_view_drag_drop; + widget_class->drag_data_received = attachment_icon_view_drag_data_received; + widget_class->popup_menu = attachment_icon_view_popup_menu; + + icon_view_class = GTK_ICON_VIEW_CLASS (class); + icon_view_class->item_activated = attachment_icon_view_item_activated; + + g_object_class_override_property ( + object_class, PROP_DRAGGING, "dragging"); + + g_object_class_override_property ( + object_class, PROP_EDITABLE, "editable"); +} + +static void +e_attachment_icon_view_init (EAttachmentIconView *icon_view) +{ + icon_view->priv = E_ATTACHMENT_ICON_VIEW_GET_PRIVATE (icon_view); + + e_attachment_view_init (E_ATTACHMENT_VIEW (icon_view)); + + gtk_icon_view_set_selection_mode ( + GTK_ICON_VIEW (icon_view), GTK_SELECTION_MULTIPLE); +} + +static void +e_attachment_icon_view_interface_init (EAttachmentViewInterface *interface) +{ + interface->get_private = attachment_icon_view_get_private; + interface->get_store = attachment_icon_view_get_store; + + interface->get_path_at_pos = attachment_icon_view_get_path_at_pos; + interface->get_selected_paths = attachment_icon_view_get_selected_paths; + interface->path_is_selected = attachment_icon_view_path_is_selected; + interface->select_path = attachment_icon_view_select_path; + interface->unselect_path = attachment_icon_view_unselect_path; + interface->select_all = attachment_icon_view_select_all; + interface->unselect_all = attachment_icon_view_unselect_all; + + interface->drag_source_set = attachment_icon_view_drag_source_set; + interface->drag_dest_set = attachment_icon_view_drag_dest_set; + interface->drag_source_unset = attachment_icon_view_drag_source_unset; + interface->drag_dest_unset = attachment_icon_view_drag_dest_unset; +} + +GtkWidget * +e_attachment_icon_view_new (void) +{ + return g_object_new (E_TYPE_ATTACHMENT_ICON_VIEW, NULL); +} diff --git a/e-util/e-attachment-icon-view.h b/e-util/e-attachment-icon-view.h new file mode 100644 index 0000000000..bd3d2109db --- /dev/null +++ b/e-util/e-attachment-icon-view.h @@ -0,0 +1,71 @@ +/* + * e-attachment-icon-view.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_ICON_VIEW_H +#define E_ATTACHMENT_ICON_VIEW_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_ICON_VIEW \ + (e_attachment_icon_view_get_type ()) +#define E_ATTACHMENT_ICON_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_ICON_VIEW, EAttachmentIconView)) +#define E_ATTACHMENT_ICON_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_ICON_VIEW, EAttachmentIconView)) +#define E_IS_ATTACHMENT_ICON_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_ICON_VIEW)) +#define E_IS_ATTACHMENT_ICON_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_ICON_VIEW)) +#define E_ATTACHMENT_ICON_VIEW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_ICON_VIEW)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentIconView EAttachmentIconView; +typedef struct _EAttachmentIconViewClass EAttachmentIconViewClass; +typedef struct _EAttachmentIconViewPrivate EAttachmentIconViewPrivate; + +struct _EAttachmentIconView { + GtkIconView parent; + EAttachmentIconViewPrivate *priv; +}; + +struct _EAttachmentIconViewClass { + GtkIconViewClass parent_class; +}; + +GType e_attachment_icon_view_get_type (void); +GtkWidget * e_attachment_icon_view_new (void); +void e_attachment_icon_view_set_default_icon_size + (gint size); +G_END_DECLS + +#endif /* E_ATTACHMENT_ICON_VIEW_H */ diff --git a/e-util/e-attachment-paned.c b/e-util/e-attachment-paned.c new file mode 100644 index 0000000000..a3c4efb187 --- /dev/null +++ b/e-util/e-attachment-paned.c @@ -0,0 +1,904 @@ +/* + * e-attachment-paned.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-paned.h" + +#include <glib/gi18n.h> + +#include "e-attachment-view.h" +#include "e-attachment-store.h" +#include "e-attachment-icon-view.h" +#include "e-attachment-tree-view.h" + +#define E_ATTACHMENT_PANED_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT_PANED, EAttachmentPanedPrivate)) + +#define NUM_VIEWS 2 + +/* Initial height of the lower pane. */ +static gint initial_height = 150; + +struct _EAttachmentPanedPrivate { + GtkTreeModel *model; + GtkWidget *expander; + GtkWidget *notebook; + GtkWidget *combo_box; + GtkWidget *controls_container; + GtkWidget *icon_view; + GtkWidget *tree_view; + GtkWidget *show_hide_label; + GtkWidget *status_icon; + GtkWidget *status_label; + GtkWidget *content_area; + + gint active_view; + gboolean expanded; + gboolean resize_toplevel; +}; + +enum { + PROP_0, + PROP_ACTIVE_VIEW, + PROP_DRAGGING, + PROP_EDITABLE, + PROP_EXPANDED, + PROP_RESIZE_TOPLEVEL +}; + +/* Forward Declarations */ +static void e_attachment_paned_interface_init + (EAttachmentViewInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EAttachmentPaned, + e_attachment_paned, + GTK_TYPE_VPANED, + G_IMPLEMENT_INTERFACE ( + E_TYPE_ATTACHMENT_VIEW, + e_attachment_paned_interface_init)) + +void +e_attachment_paned_set_default_height (gint height) +{ + initial_height = height; +} + +static void +attachment_paned_notify_cb (EAttachmentPaned *paned, + GParamSpec *pspec, + GtkExpander *expander) +{ + GtkAllocation toplevel_allocation; + GtkWidget *toplevel; + GtkWidget *child; + GtkLabel *label; + const gchar *text; + + label = GTK_LABEL (paned->priv->show_hide_label); + + /* Update the expander label. */ + if (gtk_expander_get_expanded (expander)) + text = _("Hide Attachment _Bar"); + else + text = _("Show Attachment _Bar"); + + gtk_label_set_text_with_mnemonic (label, text); + + /* Resize the top-level window if required conditions are met. + * This is based on gtk_expander_resize_toplevel(), but adapted + * to the fact our GtkExpander has no direct child widget. */ + + if (!e_attachment_paned_get_resize_toplevel (paned)) + return; + + if (!gtk_widget_get_realized (GTK_WIDGET (paned))) + return; + + child = gtk_paned_get_child2 (GTK_PANED (paned)); + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (paned)); + + if (toplevel == NULL) + return; + + if (!gtk_widget_get_realized (GTK_WIDGET (toplevel))) + return; + + gtk_widget_get_allocation (toplevel, &toplevel_allocation); + + if (gtk_expander_get_expanded (expander)) { + GtkRequisition child_requisition; + + gtk_widget_get_preferred_size ( + child, &child_requisition, NULL); + + toplevel_allocation.height += child_requisition.height; + } else { + GtkAllocation child_allocation; + + gtk_widget_get_allocation (child, &child_allocation); + + toplevel_allocation.height -= child_allocation.height; + } + + gtk_window_resize ( + GTK_WINDOW (toplevel), + toplevel_allocation.width, + toplevel_allocation.height); +} + +static void +attachment_paned_update_status (EAttachmentPaned *paned) +{ + EAttachmentView *view; + EAttachmentStore *store; + GtkExpander *expander; + GtkLabel *label; + guint num_attachments; + guint64 total_size; + gchar *display_size; + gchar *markup; + + view = E_ATTACHMENT_VIEW (paned); + store = e_attachment_view_get_store (view); + expander = GTK_EXPANDER (paned->priv->expander); + label = GTK_LABEL (paned->priv->status_label); + + num_attachments = e_attachment_store_get_num_attachments (store); + total_size = e_attachment_store_get_total_size (store); + display_size = g_format_size (total_size); + + if (total_size > 0) + markup = g_strdup_printf ( + "<b>%d</b> %s (%s)", num_attachments, ngettext ( + "Attachment", "Attachments", num_attachments), + display_size); + else + markup = g_strdup_printf ( + "<b>%d</b> %s", num_attachments, ngettext ( + "Attachment", "Attachments", num_attachments)); + gtk_label_set_markup (label, markup); + g_free (markup); + + g_free (display_size); + + if (num_attachments > 0) { + gtk_widget_show (paned->priv->status_icon); + gtk_widget_show (paned->priv->status_label); + gtk_expander_set_expanded (expander, TRUE); + } else { + gtk_widget_hide (paned->priv->status_icon); + gtk_widget_hide (paned->priv->status_label); + gtk_expander_set_expanded (expander, FALSE); + } +} + +static void +attachment_paned_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVE_VIEW: + e_attachment_paned_set_active_view ( + E_ATTACHMENT_PANED (object), + g_value_get_int (value)); + return; + + case PROP_DRAGGING: + e_attachment_view_set_dragging ( + E_ATTACHMENT_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_EDITABLE: + e_attachment_view_set_editable ( + E_ATTACHMENT_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_EXPANDED: + e_attachment_paned_set_expanded ( + E_ATTACHMENT_PANED (object), + g_value_get_boolean (value)); + return; + + case PROP_RESIZE_TOPLEVEL: + e_attachment_paned_set_resize_toplevel ( + E_ATTACHMENT_PANED (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_paned_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVE_VIEW: + g_value_set_int ( + value, + e_attachment_paned_get_active_view ( + E_ATTACHMENT_PANED (object))); + return; + + case PROP_DRAGGING: + g_value_set_boolean ( + value, + e_attachment_view_get_dragging ( + E_ATTACHMENT_VIEW (object))); + return; + + case PROP_EDITABLE: + g_value_set_boolean ( + value, + e_attachment_view_get_editable ( + E_ATTACHMENT_VIEW (object))); + return; + + case PROP_EXPANDED: + g_value_set_boolean ( + value, + e_attachment_paned_get_expanded ( + E_ATTACHMENT_PANED (object))); + return; + + case PROP_RESIZE_TOPLEVEL: + g_value_set_boolean ( + value, + e_attachment_paned_get_resize_toplevel ( + E_ATTACHMENT_PANED (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_paned_dispose (GObject *object) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (object); + + if (priv->model != NULL) { + e_attachment_store_remove_all (E_ATTACHMENT_STORE (priv->model)); + g_object_unref (priv->model); + priv->model = NULL; + } + + if (priv->expander != NULL) { + g_object_unref (priv->expander); + priv->expander = NULL; + } + + if (priv->notebook != NULL) { + g_object_unref (priv->notebook); + priv->notebook = NULL; + } + + if (priv->combo_box != NULL) { + g_object_unref (priv->combo_box); + priv->combo_box = NULL; + } + + if (priv->icon_view != NULL) { + g_object_unref (priv->icon_view); + priv->icon_view = NULL; + } + + if (priv->tree_view != NULL) { + g_object_unref (priv->tree_view); + priv->tree_view = NULL; + } + + if (priv->show_hide_label != NULL) { + g_object_unref (priv->show_hide_label); + priv->show_hide_label = NULL; + } + + if (priv->status_icon != NULL) { + g_object_unref (priv->status_icon); + priv->status_icon = NULL; + } + + if (priv->status_label != NULL) { + g_object_unref (priv->status_label); + priv->status_label = NULL; + } + + if (priv->content_area != NULL) { + g_object_unref (priv->content_area); + priv->content_area = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_attachment_paned_parent_class)->dispose (object); +} + +static void +attachment_paned_constructed (GObject *object) +{ + EAttachmentPanedPrivate *priv; + GSettings *settings; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (object); + + settings = g_settings_new ("org.gnome.evolution.shell"); + + /* Set up property-to-property bindings. */ + + g_object_bind_property ( + object, "active-view", + priv->combo_box, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "active-view", + priv->notebook, "page", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "dragging", + priv->icon_view, "dragging", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "dragging", + priv->tree_view, "dragging", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "editable", + priv->icon_view, "editable", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "editable", + priv->tree_view, "editable", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "expanded", + priv->expander, "expanded", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "expanded", + priv->combo_box, "sensitive", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + object, "expanded", + priv->notebook, "visible", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + /* Set up property-to-GSettings bindings. */ + g_settings_bind ( + settings, "attachment-view", + object, "active-view", + G_SETTINGS_BIND_DEFAULT); + + g_object_unref (settings); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_attachment_paned_parent_class)->constructed (object); +} + +static EAttachmentViewPrivate * +attachment_paned_get_private (EAttachmentView *view) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + return e_attachment_view_get_private (view); +} + +static EAttachmentStore * +attachment_paned_get_store (EAttachmentView *view) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + return e_attachment_view_get_store (view); +} + +static GtkTreePath * +attachment_paned_get_path_at_pos (EAttachmentView *view, + gint x, + gint y) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + return e_attachment_view_get_path_at_pos (view, x, y); +} + +static GList * +attachment_paned_get_selected_paths (EAttachmentView *view) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + return e_attachment_view_get_selected_paths (view); +} + +static gboolean +attachment_paned_path_is_selected (EAttachmentView *view, + GtkTreePath *path) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + return e_attachment_view_path_is_selected (view, path); +} + +static void +attachment_paned_select_path (EAttachmentView *view, + GtkTreePath *path) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + e_attachment_view_select_path (view, path); +} + +static void +attachment_paned_unselect_path (EAttachmentView *view, + GtkTreePath *path) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + e_attachment_view_unselect_path (view, path); +} + +static void +attachment_paned_select_all (EAttachmentView *view) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + e_attachment_view_select_all (view); +} + +static void +attachment_paned_unselect_all (EAttachmentView *view) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + e_attachment_view_unselect_all (view); +} + +static void +attachment_paned_update_actions (EAttachmentView *view) +{ + EAttachmentPanedPrivate *priv; + + priv = E_ATTACHMENT_PANED_GET_PRIVATE (view); + view = E_ATTACHMENT_VIEW (priv->icon_view); + + e_attachment_view_update_actions (view); +} + +static void +e_attachment_paned_class_init (EAttachmentPanedClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EAttachmentPanedPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = attachment_paned_set_property; + object_class->get_property = attachment_paned_get_property; + object_class->dispose = attachment_paned_dispose; + object_class->constructed = attachment_paned_constructed; + + g_object_class_install_property ( + object_class, + PROP_ACTIVE_VIEW, + g_param_spec_int ( + "active-view", + "Active View", + NULL, + 0, + NUM_VIEWS, + 0, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + g_object_class_override_property ( + object_class, PROP_DRAGGING, "dragging"); + + g_object_class_override_property ( + object_class, PROP_EDITABLE, "editable"); + + g_object_class_install_property ( + object_class, + PROP_EXPANDED, + g_param_spec_boolean ( + "expanded", + "Expanded", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_RESIZE_TOPLEVEL, + g_param_spec_boolean ( + "resize-toplevel", + "Resize-Toplevel", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_attachment_paned_init (EAttachmentPaned *paned) +{ + EAttachmentView *view; + GtkSizeGroup *size_group; + GtkWidget *container; + GtkWidget *widget; + GtkAction *action; + + paned->priv = E_ATTACHMENT_PANED_GET_PRIVATE (paned); + paned->priv->model = e_attachment_store_new (); + + /* Keep the expander label and combo box the same height. */ + size_group = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL); + + /* Construct the Attachment Views */ + + container = GTK_WIDGET (paned); + + widget = gtk_notebook_new (); + gtk_widget_set_size_request (widget, -1, initial_height); + gtk_notebook_set_show_tabs (GTK_NOTEBOOK (widget), FALSE); + gtk_notebook_set_show_border (GTK_NOTEBOOK (widget), FALSE); + gtk_paned_pack2 (GTK_PANED (container), widget, FALSE, FALSE); + paned->priv->notebook = g_object_ref (widget); + gtk_widget_hide (widget); + + container = paned->priv->notebook; + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_notebook_append_page (GTK_NOTEBOOK (container), widget, NULL); + gtk_widget_show (widget); + + container = widget; + + widget = e_attachment_icon_view_new (); + gtk_widget_set_can_focus (widget, TRUE); + gtk_icon_view_set_model (GTK_ICON_VIEW (widget), paned->priv->model); + gtk_container_add (GTK_CONTAINER (container), widget); + paned->priv->icon_view = g_object_ref (widget); + gtk_widget_show (widget); + + container = paned->priv->notebook; + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_notebook_append_page (GTK_NOTEBOOK (container), widget, NULL); + gtk_widget_show (widget); + + container = widget; + + widget = e_attachment_tree_view_new (); + gtk_widget_set_can_focus (widget, TRUE); + gtk_tree_view_set_model (GTK_TREE_VIEW (widget), paned->priv->model); + gtk_container_add (GTK_CONTAINER (container), widget); + paned->priv->tree_view = g_object_ref (widget); + gtk_widget_show (widget); + + /* Construct the Controls */ + + container = GTK_WIDGET (paned); + + widget = gtk_vbox_new (FALSE, 6); + gtk_paned_pack1 (GTK_PANED (container), widget, TRUE, FALSE); + paned->priv->content_area = g_object_ref (widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_hbox_new (FALSE, 6); + gtk_box_pack_end (GTK_BOX (container), widget, FALSE, FALSE, 0); + paned->priv->controls_container = widget; + gtk_widget_show (widget); + + container = widget; + + widget = gtk_expander_new (NULL); + gtk_expander_set_spacing (GTK_EXPANDER (widget), 0); + gtk_expander_set_label_fill (GTK_EXPANDER (widget), TRUE); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + paned->priv->expander = g_object_ref (widget); + gtk_widget_show (widget); + + /* The "Add Attachment" button proxies the "add" action from + * one of the two attachment views. Doesn't matter which. */ + widget = gtk_button_new (); + view = E_ATTACHMENT_VIEW (paned->priv->icon_view); + action = e_attachment_view_get_action (view, "add"); + gtk_button_set_image (GTK_BUTTON (widget), gtk_image_new ()); + gtk_activatable_set_related_action (GTK_ACTIVATABLE (widget), action); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_combo_box_text_new (); + gtk_size_group_add_widget (size_group, widget); + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (widget), _("Icon View")); + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (widget), _("List View")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + paned->priv->combo_box = g_object_ref (widget); + gtk_widget_show (widget); + + container = paned->priv->expander; + + widget = gtk_hbox_new (FALSE, 6); + gtk_size_group_add_widget (size_group, widget); + gtk_expander_set_label_widget (GTK_EXPANDER (container), widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_label_new_with_mnemonic (_("Show Attachment _Bar")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + paned->priv->show_hide_label = g_object_ref (widget); + gtk_widget_show (widget); + + widget = gtk_alignment_new (0.5, 0.5, 0.0, 1.0); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_hbox_new (FALSE, 6); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_image_new_from_icon_name ( + "mail-attachment", GTK_ICON_SIZE_MENU); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + paned->priv->status_icon = g_object_ref (widget); + gtk_widget_hide (widget); + + widget = gtk_label_new (NULL); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + paned->priv->status_label = g_object_ref (widget); + gtk_widget_hide (widget); + + g_signal_connect_swapped ( + paned->priv->expander, "notify::expanded", + G_CALLBACK (attachment_paned_notify_cb), paned); + + g_signal_connect_swapped ( + paned->priv->model, "notify::num-attachments", + G_CALLBACK (attachment_paned_update_status), paned); + + g_signal_connect_swapped ( + paned->priv->model, "notify::total-size", + G_CALLBACK (attachment_paned_update_status), paned); + + g_object_unref (size_group); +} + +static void +e_attachment_paned_interface_init (EAttachmentViewInterface *interface) +{ + interface->get_private = attachment_paned_get_private; + interface->get_store = attachment_paned_get_store; + interface->get_path_at_pos = attachment_paned_get_path_at_pos; + interface->get_selected_paths = attachment_paned_get_selected_paths; + interface->path_is_selected = attachment_paned_path_is_selected; + interface->select_path = attachment_paned_select_path; + interface->unselect_path = attachment_paned_unselect_path; + interface->select_all = attachment_paned_select_all; + interface->unselect_all = attachment_paned_unselect_all; + interface->update_actions = attachment_paned_update_actions; +} + +GtkWidget * +e_attachment_paned_new (void) +{ + return g_object_new (E_TYPE_ATTACHMENT_PANED, NULL); +} + +GtkWidget * +e_attachment_paned_get_content_area (EAttachmentPaned *paned) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_PANED (paned), NULL); + + return paned->priv->content_area; +} + +gint +e_attachment_paned_get_active_view (EAttachmentPaned *paned) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_PANED (paned), 0); + + return paned->priv->active_view; +} + +void +e_attachment_paned_set_active_view (EAttachmentPaned *paned, + gint active_view) +{ + EAttachmentView *source; + EAttachmentView *target; + + g_return_if_fail (E_IS_ATTACHMENT_PANED (paned)); + g_return_if_fail (active_view >= 0 && active_view < NUM_VIEWS); + + if (active_view == paned->priv->active_view) + return; + + paned->priv->active_view = active_view; + + /* Synchronize the item selection of the view we're + * switching TO with the view we're switching FROM. */ + if (active_view == 0) { + /* from tree view to icon view */ + source = E_ATTACHMENT_VIEW (paned->priv->tree_view); + target = E_ATTACHMENT_VIEW (paned->priv->icon_view); + } else { + /* from icon view to tree view */ + source = E_ATTACHMENT_VIEW (paned->priv->icon_view); + target = E_ATTACHMENT_VIEW (paned->priv->tree_view); + } + + e_attachment_view_sync_selection (source, target); + + g_object_notify (G_OBJECT (paned), "active-view"); +} + +gboolean +e_attachment_paned_get_expanded (EAttachmentPaned *paned) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_PANED (paned), FALSE); + + return paned->priv->expanded; +} + +void +e_attachment_paned_set_expanded (EAttachmentPaned *paned, + gboolean expanded) +{ + g_return_if_fail (E_IS_ATTACHMENT_PANED (paned)); + + if (paned->priv->expanded == expanded) + return; + + paned->priv->expanded = expanded; + + g_object_notify (G_OBJECT (paned), "expanded"); +} + +gboolean +e_attachment_paned_get_resize_toplevel (EAttachmentPaned *paned) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_PANED (paned), FALSE); + + return paned->priv->resize_toplevel; +} + +void +e_attachment_paned_set_resize_toplevel (EAttachmentPaned *paned, + gboolean resize_toplevel) +{ + g_return_if_fail (E_IS_ATTACHMENT_PANED (paned)); + + if (paned->priv->resize_toplevel == resize_toplevel) + return; + + paned->priv->resize_toplevel = resize_toplevel; + + g_object_notify (G_OBJECT (paned), "resize-toplevel"); +} + +void +e_attachment_paned_drag_data_received (EAttachmentPaned *paned, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection, + guint info, + guint time) +{ + g_return_if_fail (E_IS_ATTACHMENT_PANED (paned)); + + /* XXX Dirty hack for forwarding drop events. */ + g_signal_emit_by_name ( + paned->priv->icon_view, "drag-data-received", + context, x, y, selection, info, time); +} + +GtkWidget * +e_attachment_paned_get_controls_container (EAttachmentPaned *paned) +{ + return paned->priv->controls_container; +} + +GtkWidget * +e_attachment_paned_get_view_combo (EAttachmentPaned *paned) +{ + return paned->priv->combo_box; +} + diff --git a/e-util/e-attachment-paned.h b/e-util/e-attachment-paned.h new file mode 100644 index 0000000000..af44cd6d67 --- /dev/null +++ b/e-util/e-attachment-paned.h @@ -0,0 +1,99 @@ +/* + * e-attachment-paned.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_PANED_H +#define E_ATTACHMENT_PANED_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_PANED \ + (e_attachment_paned_get_type ()) +#define E_ATTACHMENT_PANED(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_PANED, EAttachmentPaned)) +#define E_ATTACHMENT_PANED_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_PANED, EAttachmentPanedClass)) +#define E_IS_ATTACHMENT_PANED(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_PANED)) +#define E_IS_ATTACHMENT_PANED_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_PANED)) +#define E_ATTACHMENT_PANED_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_PANED, EAttachmentPanedClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentPaned EAttachmentPaned; +typedef struct _EAttachmentPanedClass EAttachmentPanedClass; +typedef struct _EAttachmentPanedPrivate EAttachmentPanedPrivate; + +struct _EAttachmentPaned { + GtkVPaned parent; + EAttachmentPanedPrivate *priv; +}; + +struct _EAttachmentPanedClass { + GtkVPanedClass parent_class; +}; + +GType e_attachment_paned_get_type (void); +GtkWidget * e_attachment_paned_new (void); +GtkWidget * e_attachment_paned_get_content_area + (EAttachmentPaned *paned); +gint e_attachment_paned_get_active_view + (EAttachmentPaned *paned); +void e_attachment_paned_set_active_view + (EAttachmentPaned *paned, + gint active_view); +gboolean e_attachment_paned_get_expanded (EAttachmentPaned *paned); +void e_attachment_paned_set_expanded (EAttachmentPaned *paned, + gboolean expanded); +gboolean e_attachment_paned_get_resize_toplevel + (EAttachmentPaned *paned); +void e_attachment_paned_set_resize_toplevel + (EAttachmentPaned *paned, + gboolean resize_toplevel); +void e_attachment_paned_drag_data_received + (EAttachmentPaned *paned, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection, + guint info, + guint time); +GtkWidget * e_attachment_paned_get_controls_container + (EAttachmentPaned *paned); +GtkWidget * e_attachment_paned_get_view_combo + (EAttachmentPaned *paned); +void e_attachment_paned_set_default_height + (gint height); + +G_END_DECLS + +#endif /* E_ATTACHMENT_PANED_H */ diff --git a/e-util/e-attachment-store.c b/e-util/e-attachment-store.c new file mode 100644 index 0000000000..f434f5e81c --- /dev/null +++ b/e-util/e-attachment-store.c @@ -0,0 +1,1280 @@ +/* + * e-attachment-store.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-store.h" + +#include <errno.h> +#include <glib/gi18n.h> + +#include "e-mktemp.h" + +#define E_ATTACHMENT_STORE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT_STORE, EAttachmentStorePrivate)) + +struct _EAttachmentStorePrivate { + GHashTable *attachment_index; + + guint ignore_row_changed : 1; +}; + +enum { + PROP_0, + PROP_NUM_ATTACHMENTS, + PROP_NUM_LOADING, + PROP_TOTAL_SIZE +}; + +G_DEFINE_TYPE ( + EAttachmentStore, + e_attachment_store, + GTK_TYPE_LIST_STORE) + +static void +attachment_store_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_NUM_ATTACHMENTS: + g_value_set_uint ( + value, + e_attachment_store_get_num_attachments ( + E_ATTACHMENT_STORE (object))); + return; + + case PROP_NUM_LOADING: + g_value_set_uint ( + value, + e_attachment_store_get_num_loading ( + E_ATTACHMENT_STORE (object))); + return; + + case PROP_TOTAL_SIZE: + g_value_set_uint64 ( + value, + e_attachment_store_get_total_size ( + E_ATTACHMENT_STORE (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_store_dispose (GObject *object) +{ + e_attachment_store_remove_all (E_ATTACHMENT_STORE (object)); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_attachment_store_parent_class)->dispose (object); +} + +static void +attachment_store_finalize (GObject *object) +{ + EAttachmentStorePrivate *priv; + + priv = E_ATTACHMENT_STORE_GET_PRIVATE (object); + + g_hash_table_destroy (priv->attachment_index); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_attachment_store_parent_class)->finalize (object); +} + +static void +e_attachment_store_class_init (EAttachmentStoreClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EAttachmentStorePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = attachment_store_get_property; + object_class->dispose = attachment_store_dispose; + object_class->finalize = attachment_store_finalize; + + g_object_class_install_property ( + object_class, + PROP_NUM_ATTACHMENTS, + g_param_spec_uint ( + "num-attachments", + "Num Attachments", + NULL, + 0, + G_MAXUINT, + 0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_NUM_LOADING, + g_param_spec_uint ( + "num-loading", + "Num Loading", + NULL, + 0, + G_MAXUINT, + 0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_TOTAL_SIZE, + g_param_spec_uint64 ( + "total-size", + "Total Size", + NULL, + 0, + G_MAXUINT64, + 0, + G_PARAM_READABLE)); +} + +static void +e_attachment_store_init (EAttachmentStore *store) +{ + GType types[E_ATTACHMENT_STORE_NUM_COLUMNS]; + GHashTable *attachment_index; + gint column = 0; + + attachment_index = g_hash_table_new_full ( + g_direct_hash, g_direct_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) gtk_tree_row_reference_free); + + store->priv = E_ATTACHMENT_STORE_GET_PRIVATE (store); + store->priv->attachment_index = attachment_index; + + types[column++] = E_TYPE_ATTACHMENT; /* COLUMN_ATTACHMENT */ + types[column++] = G_TYPE_STRING; /* COLUMN_CAPTION */ + types[column++] = G_TYPE_STRING; /* COLUMN_CONTENT_TYPE */ + types[column++] = G_TYPE_STRING; /* COLUMN_DESCRIPTION */ + types[column++] = G_TYPE_ICON; /* COLUMN_ICON */ + types[column++] = G_TYPE_BOOLEAN; /* COLUMN_LOADING */ + types[column++] = G_TYPE_INT; /* COLUMN_PERCENT */ + types[column++] = G_TYPE_BOOLEAN; /* COLUMN_SAVING */ + types[column++] = G_TYPE_UINT64; /* COLUMN_SIZE */ + + g_assert (column == E_ATTACHMENT_STORE_NUM_COLUMNS); + + gtk_list_store_set_column_types ( + GTK_LIST_STORE (store), G_N_ELEMENTS (types), types); +} + +GtkTreeModel * +e_attachment_store_new (void) +{ + return g_object_new (E_TYPE_ATTACHMENT_STORE, NULL); +} + +void +e_attachment_store_add_attachment (EAttachmentStore *store, + EAttachment *attachment) +{ + GtkTreeRowReference *reference; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + + g_return_if_fail (E_IS_ATTACHMENT_STORE (store)); + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + gtk_list_store_append (GTK_LIST_STORE (store), &iter); + + gtk_list_store_set ( + GTK_LIST_STORE (store), &iter, + E_ATTACHMENT_STORE_COLUMN_ATTACHMENT, attachment, -1); + + model = GTK_TREE_MODEL (store); + path = gtk_tree_model_get_path (model, &iter); + reference = gtk_tree_row_reference_new (model, path); + gtk_tree_path_free (path); + + g_hash_table_insert ( + store->priv->attachment_index, + g_object_ref (attachment), reference); + + /* This lets the attachment tell us when to update. */ + e_attachment_set_reference (attachment, reference); + + g_object_freeze_notify (G_OBJECT (store)); + g_object_notify (G_OBJECT (store), "num-attachments"); + g_object_notify (G_OBJECT (store), "total-size"); + g_object_thaw_notify (G_OBJECT (store)); +} + +gboolean +e_attachment_store_remove_attachment (EAttachmentStore *store, + EAttachment *attachment) +{ + GtkTreeRowReference *reference; + GHashTable *hash_table; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), FALSE); + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + + hash_table = store->priv->attachment_index; + reference = g_hash_table_lookup (hash_table, attachment); + + if (reference == NULL) + return FALSE; + + if (!gtk_tree_row_reference_valid (reference)) { + g_hash_table_remove (hash_table, attachment); + return FALSE; + } + + e_attachment_cancel (attachment); + e_attachment_set_reference (attachment, NULL); + + model = gtk_tree_row_reference_get_model (reference); + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + + gtk_list_store_remove (GTK_LIST_STORE (store), &iter); + g_hash_table_remove (hash_table, attachment); + + g_object_freeze_notify (G_OBJECT (store)); + g_object_notify (G_OBJECT (store), "num-attachments"); + g_object_notify (G_OBJECT (store), "total-size"); + g_object_thaw_notify (G_OBJECT (store)); + + return TRUE; +} + +void +e_attachment_store_remove_all (EAttachmentStore *store) +{ + GList *list, *iter; + + g_return_if_fail (E_IS_ATTACHMENT_STORE (store)); + + if (!g_hash_table_size (store->priv->attachment_index)) + return; + + g_object_freeze_notify (G_OBJECT (store)); + + list = e_attachment_store_get_attachments (store); + for (iter = list; iter; iter = iter->next) { + EAttachment *attachment = iter->data; + + e_attachment_cancel (attachment); + g_hash_table_remove (store->priv->attachment_index, iter->data); + } + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); + + gtk_list_store_clear (GTK_LIST_STORE (store)); + + g_object_notify (G_OBJECT (store), "num-attachments"); + g_object_notify (G_OBJECT (store), "total-size"); + g_object_thaw_notify (G_OBJECT (store)); +} + +void +e_attachment_store_add_to_multipart (EAttachmentStore *store, + CamelMultipart *multipart, + const gchar *default_charset) +{ + GList *list, *iter; + + g_return_if_fail (E_IS_ATTACHMENT_STORE (store)); + g_return_if_fail (CAMEL_MULTIPART (multipart)); + + list = e_attachment_store_get_attachments (store); + + for (iter = list; iter != NULL; iter = iter->next) { + EAttachment *attachment = iter->data; + + /* Skip the attachment if it's still loading. */ + if (!e_attachment_get_loading (attachment)) + e_attachment_add_to_multipart ( + attachment, multipart, default_charset); + } + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +GList * +e_attachment_store_get_attachments (EAttachmentStore *store) +{ + GList *list = NULL; + GtkTreeModel *model; + GtkTreeIter iter; + gboolean valid; + + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), NULL); + + model = GTK_TREE_MODEL (store); + valid = gtk_tree_model_get_iter_first (model, &iter); + + while (valid) { + EAttachment *attachment; + gint column_id; + + column_id = E_ATTACHMENT_STORE_COLUMN_ATTACHMENT; + gtk_tree_model_get (model, &iter, column_id, &attachment, -1); + + list = g_list_prepend (list, attachment); + + valid = gtk_tree_model_iter_next (model, &iter); + } + + return g_list_reverse (list); +} + +guint +e_attachment_store_get_num_attachments (EAttachmentStore *store) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), 0); + + return g_hash_table_size (store->priv->attachment_index); +} + +guint +e_attachment_store_get_num_loading (EAttachmentStore *store) +{ + GList *list, *iter; + guint num_loading = 0; + + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), 0); + + list = e_attachment_store_get_attachments (store); + + for (iter = list; iter != NULL; iter = iter->next) { + EAttachment *attachment = iter->data; + + if (e_attachment_get_loading (attachment)) + num_loading++; + } + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); + + return num_loading; +} + +goffset +e_attachment_store_get_total_size (EAttachmentStore *store) +{ + GList *list, *iter; + goffset total_size = 0; + + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), 0); + + list = e_attachment_store_get_attachments (store); + + for (iter = list; iter != NULL; iter = iter->next) { + EAttachment *attachment = iter->data; + GFileInfo *file_info; + + file_info = e_attachment_get_file_info (attachment); + if (file_info != NULL) + total_size += g_file_info_get_size (file_info); + } + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); + + return total_size; +} + +void +e_attachment_store_run_load_dialog (EAttachmentStore *store, + GtkWindow *parent) +{ + GtkFileChooser *file_chooser; + GtkWidget *dialog; + GtkWidget *option; + GSList *files, *iter; + const gchar *disposition; + gboolean active; + gint response; + + g_return_if_fail (E_IS_ATTACHMENT_STORE (store)); + g_return_if_fail (GTK_IS_WINDOW (parent)); + + dialog = gtk_file_chooser_dialog_new ( + _("Add Attachment"), parent, + GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + _("A_ttach"), GTK_RESPONSE_OK, NULL); + + file_chooser = GTK_FILE_CHOOSER (dialog); + gtk_file_chooser_set_local_only (file_chooser, FALSE); + gtk_file_chooser_set_select_multiple (file_chooser, TRUE); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + gtk_window_set_icon_name (GTK_WINDOW (dialog), "mail-attachment"); + + option = gtk_check_button_new_with_mnemonic ( + _("_Suggest automatic display of attachment")); + gtk_file_chooser_set_extra_widget (file_chooser, option); + gtk_widget_show (option); + + response = gtk_dialog_run (GTK_DIALOG (dialog)); + + if (response != GTK_RESPONSE_OK) + goto exit; + + files = gtk_file_chooser_get_files (file_chooser); + active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (option)); + disposition = active ? "inline" : "attachment"; + + for (iter = files; iter != NULL; iter = g_slist_next (iter)) { + EAttachment *attachment; + GFile *file = iter->data; + + attachment = e_attachment_new (); + e_attachment_set_file (attachment, file); + e_attachment_set_disposition (attachment, disposition); + e_attachment_store_add_attachment (store, attachment); + e_attachment_load_async ( + attachment, (GAsyncReadyCallback) + e_attachment_load_handle_error, parent); + g_object_unref (attachment); + } + + g_slist_foreach (files, (GFunc) g_object_unref, NULL); + g_slist_free (files); + +exit: + gtk_widget_destroy (dialog); +} + +GFile * +e_attachment_store_run_save_dialog (EAttachmentStore *store, + GList *attachment_list, + GtkWindow *parent) +{ + GtkFileChooser *file_chooser; + GtkFileChooserAction action; + GtkWidget *dialog; + GFile *destination; + const gchar *title; + gint response; + guint length; + + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), NULL); + + length = g_list_length (attachment_list); + + if (length == 0) + return NULL; + + title = ngettext ("Save Attachment", "Save Attachments", length); + + if (length == 1) + action = GTK_FILE_CHOOSER_ACTION_SAVE; + else + action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; + + dialog = gtk_file_chooser_dialog_new ( + title, parent, action, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_SAVE, GTK_RESPONSE_OK, NULL); + + file_chooser = GTK_FILE_CHOOSER (dialog); + gtk_file_chooser_set_local_only (file_chooser, FALSE); + gtk_file_chooser_set_do_overwrite_confirmation (file_chooser, TRUE); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + gtk_window_set_icon_name (GTK_WINDOW (dialog), "mail-attachment"); + + if (action == GTK_FILE_CHOOSER_ACTION_SAVE) { + EAttachment *attachment; + GFileInfo *file_info; + const gchar *name = NULL; + + attachment = attachment_list->data; + file_info = e_attachment_get_file_info (attachment); + if (file_info != NULL) + name = g_file_info_get_display_name (file_info); + if (name == NULL) + /* Translators: Default attachment filename. */ + name = _("attachment.dat"); + gtk_file_chooser_set_current_name (file_chooser, name); + } + + response = gtk_dialog_run (GTK_DIALOG (dialog)); + + if (response == GTK_RESPONSE_OK) + destination = gtk_file_chooser_get_file (file_chooser); + else + destination = NULL; + + gtk_widget_destroy (dialog); + + return destination; +} + +/******************** e_attachment_store_get_uris_async() ********************/ + +typedef struct _UriContext UriContext; + +struct _UriContext { + GSimpleAsyncResult *simple; + GList *attachment_list; + GError *error; + gchar **uris; + gint index; +}; + +static UriContext * +attachment_store_uri_context_new (EAttachmentStore *store, + GList *attachment_list, + GAsyncReadyCallback callback, + gpointer user_data) +{ + UriContext *uri_context; + GSimpleAsyncResult *simple; + guint length; + gchar **uris; + + simple = g_simple_async_result_new ( + G_OBJECT (store), callback, user_data, + e_attachment_store_get_uris_async); + + /* Add one for NULL terminator. */ + length = g_list_length (attachment_list) + 1; + uris = g_malloc0 (sizeof (gchar *) * length); + + uri_context = g_slice_new0 (UriContext); + uri_context->simple = simple; + uri_context->attachment_list = g_list_copy (attachment_list); + uri_context->uris = uris; + + g_list_foreach ( + uri_context->attachment_list, + (GFunc) g_object_ref, NULL); + + return uri_context; +} + +static void +attachment_store_uri_context_free (UriContext *uri_context) +{ + g_object_unref (uri_context->simple); + + /* The attachment list should be empty now. */ + g_warn_if_fail (uri_context->attachment_list == NULL); + + /* So should the error. */ + g_warn_if_fail (uri_context->error == NULL); + + g_strfreev (uri_context->uris); + + g_slice_free (UriContext, uri_context); +} + +static void +attachment_store_get_uris_save_cb (EAttachment *attachment, + GAsyncResult *result, + UriContext *uri_context) +{ + GSimpleAsyncResult *simple; + GFile *file; + gchar **uris; + gchar *uri; + GError *error = NULL; + + file = e_attachment_save_finish (attachment, result, &error); + + /* Remove the attachment from the list. */ + uri_context->attachment_list = g_list_remove ( + uri_context->attachment_list, attachment); + g_object_unref (attachment); + + if (file != NULL) { + uri = g_file_get_uri (file); + uri_context->uris[uri_context->index++] = uri; + g_object_unref (file); + + } else if (error != NULL) { + /* If this is the first error, cancel the other jobs. */ + if (uri_context->error == NULL) { + g_propagate_error (&uri_context->error, error); + g_list_foreach ( + uri_context->attachment_list, + (GFunc) e_attachment_cancel, NULL); + error = NULL; + + /* Otherwise, we can only report back one error. So if + * this is something other than cancellation, dump it to + * the terminal. */ + } else if (!g_error_matches ( + error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("%s", error->message); + } + + if (error != NULL) + g_error_free (error); + + /* If there's still jobs running, let them finish. */ + if (uri_context->attachment_list != NULL) + return; + + /* Steal the URI list. */ + uris = uri_context->uris; + uri_context->uris = NULL; + + /* And the error. */ + error = uri_context->error; + uri_context->error = NULL; + + simple = uri_context->simple; + + if (error == NULL) + g_simple_async_result_set_op_res_gpointer (simple, uris, NULL); + else + g_simple_async_result_take_error (simple, error); + + g_simple_async_result_complete (simple); + + attachment_store_uri_context_free (uri_context); +} + +void +e_attachment_store_get_uris_async (EAttachmentStore *store, + GList *attachment_list, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GFile *temp_directory; + UriContext *uri_context; + GList *iter, *trash = NULL; + gchar *template; + gchar *path; + + g_return_if_fail (E_IS_ATTACHMENT_STORE (store)); + + uri_context = attachment_store_uri_context_new ( + store, attachment_list, callback, user_data); + + /* Grab the copied attachment list. */ + attachment_list = uri_context->attachment_list; + + /* First scan the list for attachments with a GFile. */ + for (iter = attachment_list; iter != NULL; iter = iter->next) { + EAttachment *attachment = iter->data; + GFile *file; + gchar *uri; + + file = e_attachment_get_file (attachment); + if (file == NULL) + continue; + + uri = g_file_get_uri (file); + uri_context->uris[uri_context->index++] = uri; + + /* Mark the list node for deletion. */ + trash = g_list_prepend (trash, iter); + g_object_unref (attachment); + } + + /* Expunge the list. */ + for (iter = trash; iter != NULL; iter = iter->next) { + GList *link = iter->data; + attachment_list = g_list_delete_link (attachment_list, link); + } + g_list_free (trash); + + uri_context->attachment_list = attachment_list; + + /* If we got them all then we're done. */ + if (attachment_list == NULL) { + GSimpleAsyncResult *simple; + gchar **uris; + + /* Steal the URI list. */ + uris = uri_context->uris; + uri_context->uris = NULL; + + simple = uri_context->simple; + g_simple_async_result_set_op_res_gpointer (simple, uris, NULL); + g_simple_async_result_complete (simple); + + attachment_store_uri_context_free (uri_context); + return; + } + + /* Any remaining attachments in the list should have MIME parts + * only, so we need to save them all to a temporary directory. + * We use a directory so the files can retain their basenames. + * XXX This could trigger a blocking temp directory cleanup. */ + template = g_strdup_printf (PACKAGE "-%s-XXXXXX", g_get_user_name ()); + path = e_mkdtemp (template); + g_free (template); + + /* XXX Let's hope errno got set properly. */ + if (path == NULL) { + GSimpleAsyncResult *simple; + + simple = uri_context->simple; + g_simple_async_result_set_error ( + simple, G_FILE_ERROR, + g_file_error_from_errno (errno), + "%s", g_strerror (errno)); + g_simple_async_result_complete (simple); + + attachment_store_uri_context_free (uri_context); + return; + } + + temp_directory = g_file_new_for_path (path); + + for (iter = attachment_list; iter != NULL; iter = iter->next) + e_attachment_save_async ( + E_ATTACHMENT (iter->data), + temp_directory, (GAsyncReadyCallback) + attachment_store_get_uris_save_cb, + uri_context); + + g_object_unref (temp_directory); + g_free (path); +} + +gchar ** +e_attachment_store_get_uris_finish (EAttachmentStore *store, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + gchar **uris; + + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), NULL); + g_return_val_if_fail (G_IS_SIMPLE_ASYNC_RESULT (result), NULL); + + simple = G_SIMPLE_ASYNC_RESULT (result); + uris = g_simple_async_result_get_op_res_gpointer (simple); + g_simple_async_result_propagate_error (simple, error); + + return uris; +} + +/********************** e_attachment_store_load_async() **********************/ + +typedef struct _LoadContext LoadContext; + +struct _LoadContext { + GSimpleAsyncResult *simple; + GList *attachment_list; + GError *error; +}; + +static LoadContext * +attachment_store_load_context_new (EAttachmentStore *store, + GList *attachment_list, + GAsyncReadyCallback callback, + gpointer user_data) +{ + LoadContext *load_context; + GSimpleAsyncResult *simple; + + simple = g_simple_async_result_new ( + G_OBJECT (store), callback, user_data, + e_attachment_store_load_async); + + load_context = g_slice_new0 (LoadContext); + load_context->simple = simple; + load_context->attachment_list = g_list_copy (attachment_list); + + g_list_foreach ( + load_context->attachment_list, + (GFunc) g_object_ref, NULL); + + return load_context; +} + +static void +attachment_store_load_context_free (LoadContext *load_context) +{ + g_object_unref (load_context->simple); + + /* The attachment list should be empty now. */ + g_warn_if_fail (load_context->attachment_list == NULL); + + /* So should the error. */ + g_warn_if_fail (load_context->error == NULL); + + g_slice_free (LoadContext, load_context); +} + +static void +attachment_store_load_ready_cb (EAttachment *attachment, + GAsyncResult *result, + LoadContext *load_context) +{ + GSimpleAsyncResult *simple; + GError *error = NULL; + + e_attachment_load_finish (attachment, result, &error); + + /* Remove the attachment from the list. */ + load_context->attachment_list = g_list_remove ( + load_context->attachment_list, attachment); + g_object_unref (attachment); + + if (error != NULL) { + /* If this is the first error, cancel the other jobs. */ + if (load_context->error == NULL) { + g_propagate_error (&load_context->error, error); + g_list_foreach ( + load_context->attachment_list, + (GFunc) e_attachment_cancel, NULL); + error = NULL; + + /* Otherwise, we can only report back one error. So if + * this is something other than cancellation, dump it to + * the terminal. */ + } else if (!g_error_matches ( + error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("%s", error->message); + } + + if (error != NULL) + g_error_free (error); + + /* If there's still jobs running, let them finish. */ + if (load_context->attachment_list != NULL) + return; + + /* Steal the error. */ + error = load_context->error; + load_context->error = NULL; + + simple = load_context->simple; + + if (error == NULL) + g_simple_async_result_set_op_res_gboolean (simple, TRUE); + else + g_simple_async_result_take_error (simple, error); + + g_simple_async_result_complete (simple); + + attachment_store_load_context_free (load_context); +} + +void +e_attachment_store_load_async (EAttachmentStore *store, + GList *attachment_list, + GAsyncReadyCallback callback, + gpointer user_data) +{ + LoadContext *load_context; + GList *iter; + + g_return_if_fail (E_IS_ATTACHMENT_STORE (store)); + + load_context = attachment_store_load_context_new ( + store, attachment_list, callback, user_data); + + if (attachment_list == NULL) { + GSimpleAsyncResult *simple; + + simple = load_context->simple; + g_simple_async_result_set_op_res_gboolean (simple, TRUE); + g_simple_async_result_complete (simple); + + attachment_store_load_context_free (load_context); + return; + } + + for (iter = attachment_list; iter != NULL; iter = iter->next) { + EAttachment *attachment = E_ATTACHMENT (iter->data); + + e_attachment_store_add_attachment (store, attachment); + + e_attachment_load_async ( + attachment, (GAsyncReadyCallback) + attachment_store_load_ready_cb, + load_context); + } +} + +gboolean +e_attachment_store_load_finish (EAttachmentStore *store, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + gboolean success; + + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), FALSE); + g_return_val_if_fail (G_IS_SIMPLE_ASYNC_RESULT (result), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + success = g_simple_async_result_get_op_res_gboolean (simple); + g_simple_async_result_propagate_error (simple, error); + + return success; +} + +/********************** e_attachment_store_save_async() **********************/ + +typedef struct _SaveContext SaveContext; + +struct _SaveContext { + GSimpleAsyncResult *simple; + GFile *destination; + gchar *filename_prefix; + GFile *fresh_directory; + GFile *trash_directory; + GList *attachment_list; + GError *error; + gchar **uris; + gint index; +}; + +static SaveContext * +attachment_store_save_context_new (EAttachmentStore *store, + GFile *destination, + const gchar *filename_prefix, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SaveContext *save_context; + GSimpleAsyncResult *simple; + GList *attachment_list; + guint length; + gchar **uris; + + simple = g_simple_async_result_new ( + G_OBJECT (store), callback, user_data, + e_attachment_store_save_async); + + attachment_list = e_attachment_store_get_attachments (store); + + /* Add one for NULL terminator. */ + length = g_list_length (attachment_list) + 1; + uris = g_malloc0 (sizeof (gchar *) * length); + + save_context = g_slice_new0 (SaveContext); + save_context->simple = simple; + save_context->destination = g_object_ref (destination); + save_context->filename_prefix = g_strdup (filename_prefix); + save_context->attachment_list = attachment_list; + save_context->uris = uris; + + return save_context; +} + +static void +attachment_store_save_context_free (SaveContext *save_context) +{ + g_object_unref (save_context->simple); + + /* The attachment list should be empty now. */ + g_warn_if_fail (save_context->attachment_list == NULL); + + /* So should the error. */ + g_warn_if_fail (save_context->error == NULL); + + if (save_context->destination) { + g_object_unref (save_context->destination); + save_context->destination = NULL; + } + + g_free (save_context->filename_prefix); + save_context->filename_prefix = NULL; + + if (save_context->fresh_directory) { + g_object_unref (save_context->fresh_directory); + save_context->fresh_directory = NULL; + } + + if (save_context->trash_directory) { + g_object_unref (save_context->trash_directory); + save_context->trash_directory = NULL; + } + + g_strfreev (save_context->uris); + + g_slice_free (SaveContext, save_context); +} + +static void +attachment_store_move_file (SaveContext *save_context, + GFile *source, + GFile *destination, + GError **error) +{ + gchar *tmpl; + gchar *path; + + g_return_if_fail (save_context != NULL); + g_return_if_fail (source != NULL); + g_return_if_fail (destination != NULL); + g_return_if_fail (error != NULL); + + /* Attachments are all saved to a temporary directory. + * Now we need to move the existing destination directory + * out of the way (if it exists). Instead of testing for + * existence we'll just attempt the move and ignore any + * G_IO_ERROR_NOT_FOUND errors. */ + + /* First, however, we need another temporary directory to + * move the existing destination directory to. Note we're + * not actually creating the directory yet, just picking a + * name for it. The usual raciness with this approach + * applies here (read up on mktemp(3)), but worst case is + * we get a spurious G_IO_ERROR_WOULD_MERGE error and the + * user has to try saving attachments again. */ + tmpl = g_strdup_printf (PACKAGE "-%s-XXXXXX", g_get_user_name ()); + path = e_mktemp (tmpl); + g_free (tmpl); + + save_context->trash_directory = g_file_new_for_path (path); + g_free (path); + + /* XXX No asynchronous move operation in GIO? */ + g_file_move ( + destination, + save_context->trash_directory, + G_FILE_COPY_NONE, NULL, NULL, NULL, error); + + if (*error != NULL && !g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return; + + g_clear_error (error); + + /* Now we can move the file from the temporary directory + * to the user-specified destination. */ + g_file_move ( + source, + destination, + G_FILE_COPY_NONE, NULL, NULL, NULL, error); +} + +static void +attachment_store_save_cb (EAttachment *attachment, + GAsyncResult *result, + SaveContext *save_context) +{ + GSimpleAsyncResult *simple; + GFile *file; + gchar **uris; + GError *error = NULL; + + file = e_attachment_save_finish (attachment, result, &error); + + /* Remove the attachment from the list. */ + save_context->attachment_list = g_list_remove ( + save_context->attachment_list, attachment); + g_object_unref (attachment); + + if (file != NULL) { + /* Assemble the file's final URI from its basename. */ + gchar *basename; + gchar *uri; + GFile *source = NULL, *destination = NULL; + + basename = g_file_get_basename (file); + g_object_unref (file); + + source = g_file_get_child (save_context->fresh_directory, basename); + + if (save_context->filename_prefix && *save_context->filename_prefix) { + gchar *tmp = basename; + + basename = g_strconcat (save_context->filename_prefix, basename, NULL); + g_free (tmp); + } + + file = save_context->destination; + destination = g_file_get_child (file, basename); + uri = g_file_get_uri (destination); + + /* move them file-by-file */ + attachment_store_move_file (save_context, source, destination, &error); + + if (!error) + save_context->uris[save_context->index++] = uri; + + g_object_unref (source); + g_object_unref (destination); + } + + if (error != NULL) { + /* If this is the first error, cancel the other jobs. */ + if (save_context->error == NULL) { + g_propagate_error (&save_context->error, error); + g_list_foreach ( + save_context->attachment_list, + (GFunc) e_attachment_cancel, NULL); + error = NULL; + + /* Otherwise, we can only report back one error. So if + * this is something other than cancellation, dump it to + * the terminal. */ + } else if (!g_error_matches ( + error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("%s", error->message); + } + + g_clear_error (&error); + + /* If there's still jobs running, let them finish. */ + if (save_context->attachment_list != NULL) + return; + + /* If an error occurred while saving, we're done. */ + if (save_context->error != NULL) { + + /* Steal the error. */ + error = save_context->error; + save_context->error = NULL; + + simple = save_context->simple; + g_simple_async_result_take_error (simple, error); + g_simple_async_result_complete (simple); + + attachment_store_save_context_free (save_context); + return; + } + + if (error != NULL) { + simple = save_context->simple; + g_simple_async_result_take_error (simple, error); + g_simple_async_result_complete (simple); + + attachment_store_save_context_free (save_context); + return; + } + + /* clean-up left directory */ + g_file_delete (save_context->fresh_directory, NULL, NULL); + + /* And the URI list. */ + uris = save_context->uris; + save_context->uris = NULL; + + simple = save_context->simple; + g_simple_async_result_set_op_res_gpointer (simple, uris, NULL); + g_simple_async_result_complete (simple); + + attachment_store_save_context_free (save_context); +} +/* + * @filename_prefix: prefix to use for a file name; can be %NULL for none + **/ +void +e_attachment_store_save_async (EAttachmentStore *store, + GFile *destination, + const gchar *filename_prefix, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SaveContext *save_context; + GList *attachment_list, *iter; + GFile *temp_directory; + gchar *template; + gchar *path; + + g_return_if_fail (E_IS_ATTACHMENT_STORE (store)); + g_return_if_fail (G_IS_FILE (destination)); + + save_context = attachment_store_save_context_new ( + store, destination, filename_prefix, callback, user_data); + + attachment_list = save_context->attachment_list; + + /* Deal with an empty attachment store. The caller will get + * an empty NULL-terminated list as opposed to NULL, to help + * distinguish it from an error. */ + if (attachment_list == NULL) { + GSimpleAsyncResult *simple; + gchar **uris; + + /* Steal the URI list. */ + uris = save_context->uris; + save_context->uris = NULL; + + simple = save_context->simple; + g_simple_async_result_set_op_res_gpointer (simple, uris, NULL); + g_simple_async_result_complete (simple); + + attachment_store_save_context_free (save_context); + return; + } + + /* Save all attachments to a temporary directory, which we'll + * then move to its proper location. We use a directory so + * files can retain their basenames. + * XXX This could trigger a blocking temp directory cleanup. */ + template = g_strdup_printf (PACKAGE "-%s-XXXXXX", g_get_user_name ()); + path = e_mkdtemp (template); + g_free (template); + + /* XXX Let's hope errno got set properly. */ + if (path == NULL) { + GSimpleAsyncResult *simple; + + simple = save_context->simple; + g_simple_async_result_set_error ( + simple, G_FILE_ERROR, + g_file_error_from_errno (errno), + "%s", g_strerror (errno)); + g_simple_async_result_complete (simple); + + attachment_store_save_context_free (save_context); + return; + } + + temp_directory = g_file_new_for_path (path); + save_context->fresh_directory = temp_directory; + g_free (path); + + for (iter = attachment_list; iter != NULL; iter = iter->next) + e_attachment_save_async ( + E_ATTACHMENT (iter->data), + temp_directory, (GAsyncReadyCallback) + attachment_store_save_cb, save_context); +} + +gchar ** +e_attachment_store_save_finish (EAttachmentStore *store, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + gchar **uris; + + g_return_val_if_fail (E_IS_ATTACHMENT_STORE (store), NULL); + g_return_val_if_fail (G_IS_SIMPLE_ASYNC_RESULT (result), NULL); + + simple = G_SIMPLE_ASYNC_RESULT (result); + uris = g_simple_async_result_get_op_res_gpointer (simple); + g_simple_async_result_propagate_error (simple, error); + + return uris; +} diff --git a/e-util/e-attachment-store.h b/e-util/e-attachment-store.h new file mode 100644 index 0000000000..a112b0e56c --- /dev/null +++ b/e-util/e-attachment-store.h @@ -0,0 +1,137 @@ +/* + * e-attachment-store.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_STORE_H +#define E_ATTACHMENT_STORE_H + +#include <gtk/gtk.h> +#include <e-util/e-attachment.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_STORE \ + (e_attachment_store_get_type ()) +#define E_ATTACHMENT_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_STORE, EAttachmentStore)) +#define E_ATTACHMENT_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_STORE, EAttachmentStoreClass)) +#define E_IS_ATTACHMENT_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_STORE)) +#define E_IS_ATTACHMENT_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_STORE)) +#define E_ATTACHMENT_STORE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_STORE, EAttachmentStoreClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentStore EAttachmentStore; +typedef struct _EAttachmentStoreClass EAttachmentStoreClass; +typedef struct _EAttachmentStorePrivate EAttachmentStorePrivate; + +struct _EAttachmentStore { + GtkListStore parent; + EAttachmentStorePrivate *priv; +}; + +struct _EAttachmentStoreClass { + GtkListStoreClass parent_class; +}; + +enum { + E_ATTACHMENT_STORE_COLUMN_ATTACHMENT, /* E_TYPE_ATTACHMENT */ + E_ATTACHMENT_STORE_COLUMN_CAPTION, /* G_TYPE_STRING */ + E_ATTACHMENT_STORE_COLUMN_CONTENT_TYPE, /* G_TYPE_STRING */ + E_ATTACHMENT_STORE_COLUMN_DESCRIPTION, /* G_TYPE_STRING */ + E_ATTACHMENT_STORE_COLUMN_ICON, /* G_TYPE_ICON */ + E_ATTACHMENT_STORE_COLUMN_LOADING, /* G_TYPE_BOOLEAN */ + E_ATTACHMENT_STORE_COLUMN_PERCENT, /* G_TYPE_INT */ + E_ATTACHMENT_STORE_COLUMN_SAVING, /* G_TYPE_BOOLEAN */ + E_ATTACHMENT_STORE_COLUMN_SIZE, /* G_TYPE_UINT64 */ + E_ATTACHMENT_STORE_NUM_COLUMNS +}; + +GType e_attachment_store_get_type (void); +GtkTreeModel * e_attachment_store_new (void); +void e_attachment_store_add_attachment + (EAttachmentStore *store, + EAttachment *attachment); +gboolean e_attachment_store_remove_attachment + (EAttachmentStore *store, + EAttachment *attachment); +void e_attachment_store_remove_all (EAttachmentStore *store); +void e_attachment_store_add_to_multipart + (EAttachmentStore *store, + CamelMultipart *multipart, + const gchar *default_charset); +GList * e_attachment_store_get_attachments + (EAttachmentStore *store); +guint e_attachment_store_get_num_attachments + (EAttachmentStore *store); +guint e_attachment_store_get_num_loading + (EAttachmentStore *store); +goffset e_attachment_store_get_total_size + (EAttachmentStore *store); +void e_attachment_store_run_load_dialog + (EAttachmentStore *store, + GtkWindow *parent); +GFile * e_attachment_store_run_save_dialog + (EAttachmentStore *store, + GList *attachment_list, + GtkWindow *parent); + +/* Asynchronous Operations */ +void e_attachment_store_get_uris_async + (EAttachmentStore *store, + GList *attachment_list, + GAsyncReadyCallback callback, + gpointer user_data); +gchar ** e_attachment_store_get_uris_finish + (EAttachmentStore *store, + GAsyncResult *result, + GError **error); +void e_attachment_store_load_async (EAttachmentStore *store, + GList *attachment_list, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_attachment_store_load_finish (EAttachmentStore *store, + GAsyncResult *result, + GError **error); +void e_attachment_store_save_async (EAttachmentStore *store, + GFile *destination, + const gchar *filename_prefix, + GAsyncReadyCallback callback, + gpointer user_data); +gchar ** e_attachment_store_save_finish (EAttachmentStore *store, + GAsyncResult *result, + GError **error); + +G_END_DECLS + +#endif /* E_ATTACHMENT_STORE_H */ + diff --git a/e-util/e-attachment-tree-view.c b/e-util/e-attachment-tree-view.c new file mode 100644 index 0000000000..b73751fbbd --- /dev/null +++ b/e-util/e-attachment-tree-view.c @@ -0,0 +1,623 @@ +/* + * e-attachment-tree-view.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-tree-view.h" + +#include <glib/gi18n.h> +#include <libebackend/libebackend.h> + +#include "e-attachment.h" +#include "e-attachment-store.h" +#include "e-attachment-view.h" + +#define E_ATTACHMENT_TREE_VIEW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT_TREE_VIEW, EAttachmentTreeViewPrivate)) + +struct _EAttachmentTreeViewPrivate { + EAttachmentViewPrivate view_priv; +}; + +enum { + PROP_0, + PROP_DRAGGING, + PROP_EDITABLE +}; + +/* Forward Declarations */ +static void e_attachment_tree_view_interface_init + (EAttachmentViewInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EAttachmentTreeView, + e_attachment_tree_view, + GTK_TYPE_TREE_VIEW, + G_IMPLEMENT_INTERFACE ( + E_TYPE_ATTACHMENT_VIEW, + e_attachment_tree_view_interface_init) + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +static void +attachment_tree_view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_DRAGGING: + e_attachment_view_set_dragging ( + E_ATTACHMENT_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_EDITABLE: + e_attachment_view_set_editable ( + E_ATTACHMENT_VIEW (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_tree_view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_DRAGGING: + g_value_set_boolean ( + value, e_attachment_view_get_dragging ( + E_ATTACHMENT_VIEW (object))); + return; + + case PROP_EDITABLE: + g_value_set_boolean ( + value, e_attachment_view_get_editable ( + E_ATTACHMENT_VIEW (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_tree_view_dispose (GObject *object) +{ + e_attachment_view_dispose (E_ATTACHMENT_VIEW (object)); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_attachment_tree_view_parent_class)->dispose (object); +} + +static void +attachment_tree_view_finalize (GObject *object) +{ + e_attachment_view_finalize (E_ATTACHMENT_VIEW (object)); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_attachment_tree_view_parent_class)->finalize (object); +} + +static void +attachment_tree_view_render_size (GtkTreeViewColumn *column, + GtkCellRenderer *renderer, + GtkTreeModel *model, + GtkTreeIter *iter) +{ + gchar *display_size = NULL; + gint column_id; + guint64 size; + + column_id = E_ATTACHMENT_STORE_COLUMN_SIZE; + gtk_tree_model_get (model, iter, column_id, &size, -1); + + if (size > 0) + display_size = g_format_size ((goffset) size); + + g_object_set (renderer, "text", display_size, NULL); + + g_free (display_size); +} + +static gboolean +attachment_tree_view_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (e_attachment_view_button_press_event (view, event)) + return TRUE; + + /* Chain up to parent's button_press_event() method. */ + return GTK_WIDGET_CLASS (e_attachment_tree_view_parent_class)-> + button_press_event (widget, event); +} + +static gboolean +attachment_tree_view_button_release_event (GtkWidget *widget, + GdkEventButton *event) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (e_attachment_view_button_release_event (view, event)) + return TRUE; + + /* Chain up to parent's button_release_event() method. */ + return GTK_WIDGET_CLASS (e_attachment_tree_view_parent_class)-> + button_release_event (widget, event); +} + +static gboolean +attachment_tree_view_motion_notify_event (GtkWidget *widget, + GdkEventMotion *event) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (e_attachment_view_motion_notify_event (view, event)) + return TRUE; + + /* Chain up to parent's motion_notify_event() method. */ + return GTK_WIDGET_CLASS (e_attachment_tree_view_parent_class)-> + motion_notify_event (widget, event); +} + +static gboolean +attachment_tree_view_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (e_attachment_view_key_press_event (view, event)) + return TRUE; + + /* Chain up to parent's key_press_event() method. */ + return GTK_WIDGET_CLASS (e_attachment_tree_view_parent_class)-> + key_press_event (widget, event); +} + +static void +attachment_tree_view_drag_begin (GtkWidget *widget, + GdkDragContext *context) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + /* Chain up to parent's drag_begin() method. */ + GTK_WIDGET_CLASS (e_attachment_tree_view_parent_class)-> + drag_begin (widget, context); + + e_attachment_view_drag_begin (view, context); +} + +static void +attachment_tree_view_drag_end (GtkWidget *widget, + GdkDragContext *context) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + /* Chain up to parent's drag_end() method. */ + GTK_WIDGET_CLASS (e_attachment_tree_view_parent_class)-> + drag_end (widget, context); + + e_attachment_view_drag_end (view, context); +} + +static void +attachment_tree_view_drag_data_get (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection, + guint info, + guint time) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + e_attachment_view_drag_data_get ( + view, context, selection, info, time); +} + +static gboolean +attachment_tree_view_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + return e_attachment_view_drag_motion (view, context, x, y, time); +} + +static gboolean +attachment_tree_view_drag_drop (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + if (!e_attachment_view_drag_drop (view, context, x, y, time)) + return FALSE; + + /* Chain up to parent's drag_drop() method. */ + return GTK_WIDGET_CLASS (e_attachment_tree_view_parent_class)-> + drag_drop (widget, context, x, y, time); +} + +static void +attachment_tree_view_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection, + guint info, + guint time) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + e_attachment_view_drag_data_received ( + view, context, x, y, selection, info, time); +} + +static gboolean +attachment_tree_view_popup_menu (GtkWidget *widget) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (widget); + + e_attachment_view_show_popup_menu (view, NULL, NULL, NULL); + + return TRUE; +} + +static void +attachment_tree_view_row_activated (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column) +{ + EAttachmentView *view = E_ATTACHMENT_VIEW (tree_view); + + e_attachment_view_open_path (view, path, NULL); +} + +static EAttachmentViewPrivate * +attachment_tree_view_get_private (EAttachmentView *view) +{ + EAttachmentTreeViewPrivate *priv; + + priv = E_ATTACHMENT_TREE_VIEW_GET_PRIVATE (view); + + return &priv->view_priv; +} + +static EAttachmentStore * +attachment_tree_view_get_store (EAttachmentView *view) +{ + GtkTreeView *tree_view; + GtkTreeModel *model; + + tree_view = GTK_TREE_VIEW (view); + model = gtk_tree_view_get_model (tree_view); + + return E_ATTACHMENT_STORE (model); +} + +static GtkTreePath * +attachment_tree_view_get_path_at_pos (EAttachmentView *view, + gint x, + gint y) +{ + GtkTreeView *tree_view; + GtkTreePath *path; + gboolean row_exists; + + tree_view = GTK_TREE_VIEW (view); + + row_exists = gtk_tree_view_get_path_at_pos ( + tree_view, x, y, &path, NULL, NULL, NULL); + + return row_exists ? path : NULL; +} + +static GList * +attachment_tree_view_get_selected_paths (EAttachmentView *view) +{ + GtkTreeView *tree_view; + GtkTreeSelection *selection; + + tree_view = GTK_TREE_VIEW (view); + selection = gtk_tree_view_get_selection (tree_view); + + return gtk_tree_selection_get_selected_rows (selection, NULL); +} + +static gboolean +attachment_tree_view_path_is_selected (EAttachmentView *view, + GtkTreePath *path) +{ + GtkTreeView *tree_view; + GtkTreeSelection *selection; + + tree_view = GTK_TREE_VIEW (view); + selection = gtk_tree_view_get_selection (tree_view); + + return gtk_tree_selection_path_is_selected (selection, path); +} + +static void +attachment_tree_view_select_path (EAttachmentView *view, + GtkTreePath *path) +{ + GtkTreeView *tree_view; + GtkTreeSelection *selection; + + tree_view = GTK_TREE_VIEW (view); + selection = gtk_tree_view_get_selection (tree_view); + + gtk_tree_selection_select_path (selection, path); +} + +static void +attachment_tree_view_unselect_path (EAttachmentView *view, + GtkTreePath *path) +{ + GtkTreeView *tree_view; + GtkTreeSelection *selection; + + tree_view = GTK_TREE_VIEW (view); + selection = gtk_tree_view_get_selection (tree_view); + + gtk_tree_selection_unselect_path (selection, path); +} + +static void +attachment_tree_view_select_all (EAttachmentView *view) +{ + GtkTreeView *tree_view; + GtkTreeSelection *selection; + + tree_view = GTK_TREE_VIEW (view); + selection = gtk_tree_view_get_selection (tree_view); + + gtk_tree_selection_select_all (selection); +} + +static void +attachment_tree_view_unselect_all (EAttachmentView *view) +{ + GtkTreeView *tree_view; + GtkTreeSelection *selection; + + tree_view = GTK_TREE_VIEW (view); + selection = gtk_tree_view_get_selection (tree_view); + + gtk_tree_selection_unselect_all (selection); +} + +static void +attachment_tree_view_drag_source_set (EAttachmentView *view, + GdkModifierType start_button_mask, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions) +{ + GtkTreeView *tree_view; + + tree_view = GTK_TREE_VIEW (view); + + gtk_tree_view_enable_model_drag_source ( + tree_view, start_button_mask, targets, n_targets, actions); +} + +static void +attachment_tree_view_drag_dest_set (EAttachmentView *view, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions) +{ + GtkTreeView *tree_view; + + tree_view = GTK_TREE_VIEW (view); + + gtk_tree_view_enable_model_drag_dest ( + tree_view, targets, n_targets, actions); +} + +static void +attachment_tree_view_drag_source_unset (EAttachmentView *view) +{ + GtkTreeView *tree_view; + + tree_view = GTK_TREE_VIEW (view); + + gtk_tree_view_unset_rows_drag_source (tree_view); +} + +static void +attachment_tree_view_drag_dest_unset (EAttachmentView *view) +{ + GtkTreeView *tree_view; + + tree_view = GTK_TREE_VIEW (view); + + gtk_tree_view_unset_rows_drag_dest (tree_view); +} + +static void +e_attachment_tree_view_class_init (EAttachmentTreeViewClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkTreeViewClass *tree_view_class; + + g_type_class_add_private (class, sizeof (EAttachmentViewPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = attachment_tree_view_set_property; + object_class->get_property = attachment_tree_view_get_property; + object_class->dispose = attachment_tree_view_dispose; + object_class->finalize = attachment_tree_view_finalize; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->button_press_event = attachment_tree_view_button_press_event; + widget_class->button_release_event = attachment_tree_view_button_release_event; + widget_class->motion_notify_event = attachment_tree_view_motion_notify_event; + widget_class->key_press_event = attachment_tree_view_key_press_event; + widget_class->drag_begin = attachment_tree_view_drag_begin; + widget_class->drag_end = attachment_tree_view_drag_end; + widget_class->drag_data_get = attachment_tree_view_drag_data_get; + widget_class->drag_motion = attachment_tree_view_drag_motion; + widget_class->drag_drop = attachment_tree_view_drag_drop; + widget_class->drag_data_received = attachment_tree_view_drag_data_received; + widget_class->popup_menu = attachment_tree_view_popup_menu; + + tree_view_class = GTK_TREE_VIEW_CLASS (class); + tree_view_class->row_activated = attachment_tree_view_row_activated; + + g_object_class_override_property ( + object_class, PROP_DRAGGING, "dragging"); + + g_object_class_override_property ( + object_class, PROP_EDITABLE, "editable"); +} + +static void +e_attachment_tree_view_init (EAttachmentTreeView *tree_view) +{ + GtkTreeSelection *selection; + GtkTreeViewColumn *column; + GtkCellRenderer *renderer; + + tree_view->priv = E_ATTACHMENT_TREE_VIEW_GET_PRIVATE (tree_view); + + e_attachment_view_init (E_ATTACHMENT_VIEW (tree_view)); + + gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (tree_view), TRUE); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE); + + /* Name Column */ + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_expand (column, TRUE); + gtk_tree_view_column_set_spacing (column, 3); + gtk_tree_view_column_set_title (column, _("Description")); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); + + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + + g_object_set (renderer, "stock-size", GTK_ICON_SIZE_MENU, NULL); + + gtk_tree_view_column_add_attribute ( + column, renderer, "gicon", + E_ATTACHMENT_STORE_COLUMN_ICON); + + renderer = gtk_cell_renderer_text_new (); + g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + + gtk_tree_view_column_add_attribute ( + column, renderer, "text", + E_ATTACHMENT_STORE_COLUMN_DESCRIPTION); + + renderer = gtk_cell_renderer_progress_new (); + g_object_set (renderer, "text", _("Loading"), NULL); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + + gtk_tree_view_column_add_attribute ( + column, renderer, "value", + E_ATTACHMENT_STORE_COLUMN_PERCENT); + + gtk_tree_view_column_add_attribute ( + column, renderer, "visible", + E_ATTACHMENT_STORE_COLUMN_LOADING); + + renderer = gtk_cell_renderer_progress_new (); + g_object_set (renderer, "text", _("Saving"), NULL); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + + gtk_tree_view_column_add_attribute ( + column, renderer, "value", + E_ATTACHMENT_STORE_COLUMN_PERCENT); + + gtk_tree_view_column_add_attribute ( + column, renderer, "visible", + E_ATTACHMENT_STORE_COLUMN_SAVING); + + /* Size Column */ + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (column, _("Size")); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); + + renderer = gtk_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + + gtk_tree_view_column_set_cell_data_func ( + column, renderer, (GtkTreeCellDataFunc) + attachment_tree_view_render_size, NULL, NULL); + + /* Type Column */ + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (column, _("Type")); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); + + renderer = gtk_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + + gtk_tree_view_column_add_attribute ( + column, renderer, "text", + E_ATTACHMENT_STORE_COLUMN_CONTENT_TYPE); + + e_extensible_load_extensions (E_EXTENSIBLE (tree_view)); +} + +static void +e_attachment_tree_view_interface_init (EAttachmentViewInterface *interface) +{ + interface->get_private = attachment_tree_view_get_private; + interface->get_store = attachment_tree_view_get_store; + + interface->get_path_at_pos = attachment_tree_view_get_path_at_pos; + interface->get_selected_paths = attachment_tree_view_get_selected_paths; + interface->path_is_selected = attachment_tree_view_path_is_selected; + interface->select_path = attachment_tree_view_select_path; + interface->unselect_path = attachment_tree_view_unselect_path; + interface->select_all = attachment_tree_view_select_all; + interface->unselect_all = attachment_tree_view_unselect_all; + + interface->drag_source_set = attachment_tree_view_drag_source_set; + interface->drag_dest_set = attachment_tree_view_drag_dest_set; + interface->drag_source_unset = attachment_tree_view_drag_source_unset; + interface->drag_dest_unset = attachment_tree_view_drag_dest_unset; +} + +GtkWidget * +e_attachment_tree_view_new (void) +{ + return g_object_new (E_TYPE_ATTACHMENT_TREE_VIEW, NULL); +} diff --git a/e-util/e-attachment-tree-view.h b/e-util/e-attachment-tree-view.h new file mode 100644 index 0000000000..416a09b7f6 --- /dev/null +++ b/e-util/e-attachment-tree-view.h @@ -0,0 +1,70 @@ +/* + * e-attachment-tree-view.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_TREE_VIEW_H +#define E_ATTACHMENT_TREE_VIEW_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_TREE_VIEW \ + (e_attachment_tree_view_get_type ()) +#define E_ATTACHMENT_TREE_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_TREE_VIEW, EAttachmentTreeView)) +#define E_ATTACHMENT_TREE_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_TREE_VIEW, EAttachmentTreeViewClass)) +#define E_IS_ATTACHMENT_TREE_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_TREE_VIEW)) +#define E_IS_ATTACHMENT_TREE_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_TREE_VIEW)) +#define E_ATTACHMENT_TREE_VIEW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT_TREE_VIEW, EAttachmentTreeViewClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentTreeView EAttachmentTreeView; +typedef struct _EAttachmentTreeViewClass EAttachmentTreeViewClass; +typedef struct _EAttachmentTreeViewPrivate EAttachmentTreeViewPrivate; + +struct _EAttachmentTreeView { + GtkTreeView parent; + EAttachmentTreeViewPrivate *priv; +}; + +struct _EAttachmentTreeViewClass { + GtkTreeViewClass parent_class; +}; + +GType e_attachment_tree_view_get_type (void); +GtkWidget * e_attachment_tree_view_new (void); + +G_END_DECLS + +#endif /* E_ATTACHMENT_TREE_VIEW_H */ diff --git a/e-util/e-attachment-view.c b/e-util/e-attachment-view.c new file mode 100644 index 0000000000..e468c14120 --- /dev/null +++ b/e-util/e-attachment-view.c @@ -0,0 +1,1906 @@ +/* + * e-attachment-view.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment-view.h" + +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include "e-attachment-dialog.h" +#include "e-attachment-handler-image.h" +#include "e-attachment-handler-sendto.h" +#include "e-misc-utils.h" +#include "e-selection.h" +#include "e-ui-manager.h" + +enum { + UPDATE_ACTIONS, + LAST_SIGNAL +}; + +/* Note: Do not use the info field. */ +static GtkTargetEntry target_table[] = { + { (gchar *) "_NETSCAPE_URL", 0, 0 } +}; + +static const gchar *ui = +"<ui>" +" <popup name='context'>" +" <menuitem action='cancel'/>" +" <menuitem action='save-as'/>" +" <menuitem action='remove'/>" +" <menuitem action='properties'/>" +" <separator/>" +" <placeholder name='inline-actions'>" +" <menuitem action='show'/>" +" <menuitem action='show-all'/>" +" <separator/>" +" <menuitem action='hide'/>" +" <menuitem action='hide-all'/>" +" </placeholder>" +" <separator/>" +" <placeholder name='custom-actions'/>" +" <separator/>" +" <menuitem action='add'/>" +" <separator/>" +" <placeholder name='open-actions'/>" +" <menuitem action='open-with'/>" +" </popup>" +"</ui>"; + +static gulong signals[LAST_SIGNAL]; + +G_DEFINE_INTERFACE ( + EAttachmentView, + e_attachment_view, + GTK_TYPE_WIDGET) + +static void +action_add_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachmentStore *store; + gpointer parent; + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + store = e_attachment_view_get_store (view); + e_attachment_store_run_load_dialog (store, parent); +} + +static void +action_cancel_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachment *attachment; + GList *list; + + list = e_attachment_view_get_selected_attachments (view); + g_return_if_fail (g_list_length (list) == 1); + attachment = list->data; + + e_attachment_cancel (attachment); + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +static void +action_hide_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachment *attachment; + GList *list; + + list = e_attachment_view_get_selected_attachments (view); + g_return_if_fail (g_list_length (list) == 1); + attachment = list->data; + + e_attachment_set_shown (attachment, FALSE); + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +static void +action_hide_all_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachmentStore *store; + GList *list, *iter; + + store = e_attachment_view_get_store (view); + list = e_attachment_store_get_attachments (store); + + for (iter = list; iter != NULL; iter = iter->next) { + EAttachment *attachment; + + attachment = E_ATTACHMENT (iter->data); + e_attachment_set_shown (attachment, FALSE); + } + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +static void +action_open_with_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachment *attachment; + EAttachmentStore *store; + GtkWidget *dialog; + GtkTreePath *path; + GtkTreeIter iter; + GAppInfo *app_info = NULL; + GFileInfo *file_info; + GList *list; + gpointer parent; + const gchar *content_type; + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + list = e_attachment_view_get_selected_paths (view); + g_return_if_fail (g_list_length (list) == 1); + path = list->data; + + store = e_attachment_view_get_store (view); + gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, path); + gtk_tree_model_get ( + GTK_TREE_MODEL (store), &iter, + E_ATTACHMENT_STORE_COLUMN_ATTACHMENT, &attachment, -1); + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + file_info = e_attachment_get_file_info (attachment); + g_return_if_fail (file_info != NULL); + + content_type = g_file_info_get_content_type (file_info); + + dialog = gtk_app_chooser_dialog_new_for_content_type ( + parent, 0, content_type); + if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_OK) { + GtkAppChooser *app_chooser = GTK_APP_CHOOSER (dialog); + app_info = gtk_app_chooser_get_app_info (app_chooser); + } + gtk_widget_destroy (dialog); + + if (app_info != NULL) { + e_attachment_view_open_path (view, path, app_info); + g_object_unref (app_info); + } + + g_list_foreach (list, (GFunc) gtk_tree_path_free, NULL); + g_list_free (list); +} + +static void +action_open_with_app_info_cb (GtkAction *action, + EAttachmentView *view) +{ + GAppInfo *app_info; + GtkTreePath *path; + GList *list; + + list = e_attachment_view_get_selected_paths (view); + g_return_if_fail (g_list_length (list) == 1); + path = list->data; + + app_info = g_object_get_data (G_OBJECT (action), "app-info"); + g_return_if_fail (G_IS_APP_INFO (app_info)); + + e_attachment_view_open_path (view, path, app_info); + + g_list_foreach (list, (GFunc) gtk_tree_path_free, NULL); + g_list_free (list); +} + +static void +action_properties_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachment *attachment; + GtkWidget *dialog; + GList *list; + gpointer parent; + + list = e_attachment_view_get_selected_attachments (view); + g_return_if_fail (g_list_length (list) == 1); + attachment = list->data; + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + dialog = e_attachment_dialog_new (parent, attachment); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +static void +action_remove_cb (GtkAction *action, + EAttachmentView *view) +{ + e_attachment_view_remove_selected (view, FALSE); +} + +static void +action_save_all_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachmentStore *store; + GList *list, *iter; + GFile *destination; + gpointer parent; + + store = e_attachment_view_get_store (view); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + /* XXX We lose the previous selection. */ + e_attachment_view_select_all (view); + list = e_attachment_view_get_selected_attachments (view); + e_attachment_view_unselect_all (view); + + destination = e_attachment_store_run_save_dialog ( + store, list, parent); + + if (destination == NULL) + goto exit; + + for (iter = list; iter != NULL; iter = iter->next) { + EAttachment *attachment = iter->data; + + e_attachment_save_async ( + attachment, destination, (GAsyncReadyCallback) + e_attachment_save_handle_error, parent); + } + + g_object_unref (destination); + +exit: + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +static void +action_save_as_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachmentStore *store; + GList *list, *iter; + GFile *destination; + gpointer parent; + + store = e_attachment_view_get_store (view); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + list = e_attachment_view_get_selected_attachments (view); + + destination = e_attachment_store_run_save_dialog ( + store, list, parent); + + if (destination == NULL) + goto exit; + + for (iter = list; iter != NULL; iter = iter->next) { + EAttachment *attachment = iter->data; + + e_attachment_save_async ( + attachment, destination, (GAsyncReadyCallback) + e_attachment_save_handle_error, parent); + } + + g_object_unref (destination); + +exit: + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +static void +action_show_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachment *attachment; + GList *list; + + list = e_attachment_view_get_selected_attachments (view); + g_return_if_fail (g_list_length (list) == 1); + attachment = list->data; + + e_attachment_set_shown (attachment, TRUE); + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +static void +action_show_all_cb (GtkAction *action, + EAttachmentView *view) +{ + EAttachmentStore *store; + GList *list, *iter; + + store = e_attachment_view_get_store (view); + list = e_attachment_store_get_attachments (store); + + for (iter = list; iter != NULL; iter = iter->next) { + EAttachment *attachment; + + attachment = E_ATTACHMENT (iter->data); + e_attachment_set_shown (attachment, TRUE); + } + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +static GtkActionEntry standard_entries[] = { + + { "cancel", + GTK_STOCK_CANCEL, + NULL, + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_cancel_cb) }, + + { "open-with", + NULL, + N_("Open With Other Application..."), + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_open_with_cb) }, + + { "save-all", + GTK_STOCK_SAVE_AS, + N_("S_ave All"), + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_save_all_cb) }, + + { "save-as", + GTK_STOCK_SAVE_AS, + NULL, + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_save_as_cb) }, + + /* Alternate "save-all" label, for when + * the attachment store has one row. */ + { "save-one", + GTK_STOCK_SAVE_AS, + NULL, + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_save_all_cb) }, +}; + +static GtkActionEntry editable_entries[] = { + + { "add", + GTK_STOCK_ADD, + N_("A_dd Attachment..."), + NULL, + N_("Attach a file"), + G_CALLBACK (action_add_cb) }, + + { "properties", + GTK_STOCK_PROPERTIES, + NULL, + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_properties_cb) }, + + { "remove", + GTK_STOCK_REMOVE, + NULL, + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_remove_cb) } +}; + +static GtkActionEntry inline_entries[] = { + + { "hide", + NULL, + N_("_Hide"), + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_hide_cb) }, + + { "hide-all", + NULL, + N_("Hid_e All"), + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_hide_all_cb) }, + + { "show", + NULL, + N_("_View Inline"), + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_show_cb) }, + + { "show-all", + NULL, + N_("Vie_w All Inline"), + NULL, + NULL, /* XXX Add a tooltip! */ + G_CALLBACK (action_show_all_cb) } +}; + +static void +attachment_view_netscape_url (EAttachmentView *view, + GdkDragContext *drag_context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time) +{ + static GdkAtom atom = GDK_NONE; + EAttachmentStore *store; + EAttachment *attachment; + const gchar *data; + gpointer parent; + gchar *copied_data; + gchar **strv; + gint length; + + if (G_UNLIKELY (atom == GDK_NONE)) + atom = gdk_atom_intern_static_string ("_NETSCAPE_URL"); + + if (gtk_selection_data_get_target (selection_data) != atom) + return; + + g_signal_stop_emission_by_name (view, "drag-data-received"); + + /* _NETSCAPE_URL is represented as "URI\nTITLE" */ + + data = (const gchar *) gtk_selection_data_get_data (selection_data); + length = gtk_selection_data_get_length (selection_data); + + copied_data = g_strndup (data, length); + strv = g_strsplit (copied_data, "\n", 2); + g_free (copied_data); + + store = e_attachment_view_get_store (view); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + attachment = e_attachment_new_for_uri (strv[0]); + e_attachment_store_add_attachment (store, attachment); + e_attachment_load_async ( + attachment, (GAsyncReadyCallback) + e_attachment_load_handle_error, parent); + g_object_unref (attachment); + + g_strfreev (strv); + + gtk_drag_finish (drag_context, TRUE, FALSE, time); +} + +static void +attachment_view_text_calendar (EAttachmentView *view, + GdkDragContext *drag_context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time) +{ + EAttachmentStore *store; + EAttachment *attachment; + CamelMimePart *mime_part; + GdkAtom data_type; + GdkAtom target; + const gchar *data; + gpointer parent; + gchar *content_type; + gint length; + + target = gtk_selection_data_get_target (selection_data); + if (!e_targets_include_calendar (&target, 1)) + return; + + g_signal_stop_emission_by_name (view, "drag-data-received"); + + data = (const gchar *) gtk_selection_data_get_data (selection_data); + length = gtk_selection_data_get_length (selection_data); + data_type = gtk_selection_data_get_data_type (selection_data); + + mime_part = camel_mime_part_new (); + + content_type = gdk_atom_name (data_type); + camel_mime_part_set_content (mime_part, data, length, content_type); + camel_mime_part_set_disposition (mime_part, "inline"); + g_free (content_type); + + store = e_attachment_view_get_store (view); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + attachment = e_attachment_new (); + e_attachment_set_mime_part (attachment, mime_part); + e_attachment_store_add_attachment (store, attachment); + e_attachment_load_async ( + attachment, (GAsyncReadyCallback) + e_attachment_load_handle_error, parent); + g_object_unref (attachment); + + g_object_unref (mime_part); + + gtk_drag_finish (drag_context, TRUE, FALSE, time); +} + +static void +attachment_view_text_x_vcard (EAttachmentView *view, + GdkDragContext *drag_context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time) +{ + EAttachmentStore *store; + EAttachment *attachment; + CamelMimePart *mime_part; + GdkAtom data_type; + GdkAtom target; + const gchar *data; + gpointer parent; + gchar *content_type; + gint length; + + target = gtk_selection_data_get_target (selection_data); + if (!e_targets_include_directory (&target, 1)) + return; + + g_signal_stop_emission_by_name (view, "drag-data-received"); + + data = (const gchar *) gtk_selection_data_get_data (selection_data); + length = gtk_selection_data_get_length (selection_data); + data_type = gtk_selection_data_get_data_type (selection_data); + + mime_part = camel_mime_part_new (); + + content_type = gdk_atom_name (data_type); + camel_mime_part_set_content (mime_part, data, length, content_type); + camel_mime_part_set_disposition (mime_part, "inline"); + g_free (content_type); + + store = e_attachment_view_get_store (view); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + attachment = e_attachment_new (); + e_attachment_set_mime_part (attachment, mime_part); + e_attachment_store_add_attachment (store, attachment); + e_attachment_load_async ( + attachment, (GAsyncReadyCallback) + e_attachment_load_handle_error, parent); + g_object_unref (attachment); + + g_object_unref (mime_part); + + gtk_drag_finish (drag_context, TRUE, FALSE, time); +} + +static void +attachment_view_uris (EAttachmentView *view, + GdkDragContext *drag_context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time) +{ + EAttachmentStore *store; + gpointer parent; + gchar **uris; + gint ii; + + uris = gtk_selection_data_get_uris (selection_data); + + if (uris == NULL) + return; + + g_signal_stop_emission_by_name (view, "drag-data-received"); + + store = e_attachment_view_get_store (view); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + for (ii = 0; uris[ii] != NULL; ii++) { + EAttachment *attachment; + + attachment = e_attachment_new_for_uri (uris[ii]); + e_attachment_store_add_attachment (store, attachment); + e_attachment_load_async ( + attachment, (GAsyncReadyCallback) + e_attachment_load_handle_error, parent); + g_object_unref (attachment); + } + + g_strfreev (uris); + + gtk_drag_finish (drag_context, TRUE, FALSE, time); +} + +static void +attachment_view_update_actions (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + EAttachment *attachment; + EAttachmentStore *store; + GtkActionGroup *action_group; + GtkAction *action; + GList *list, *iter; + guint n_shown = 0; + guint n_hidden = 0; + guint n_selected; + gboolean busy = FALSE; + gboolean can_show = FALSE; + gboolean shown = FALSE; + gboolean visible; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + priv = e_attachment_view_get_private (view); + + store = e_attachment_view_get_store (view); + list = e_attachment_store_get_attachments (store); + + for (iter = list; iter != NULL; iter = iter->next) { + attachment = iter->data; + + if (!e_attachment_get_can_show (attachment)) + continue; + + if (e_attachment_get_shown (attachment)) + n_shown++; + else + n_hidden++; + } + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); + + list = e_attachment_view_get_selected_attachments (view); + n_selected = g_list_length (list); + + if (n_selected == 1) { + attachment = g_object_ref (list->data); + busy |= e_attachment_get_loading (attachment); + busy |= e_attachment_get_saving (attachment); + can_show = e_attachment_get_can_show (attachment); + shown = e_attachment_get_shown (attachment); + } else + attachment = NULL; + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); + + action = e_attachment_view_get_action (view, "cancel"); + gtk_action_set_visible (action, busy); + + action = e_attachment_view_get_action (view, "hide"); + gtk_action_set_visible (action, can_show && shown); + + /* Show this action if there are multiple viewable + * attachments, and at least one of them is shown. */ + visible = (n_shown + n_hidden > 1) && (n_shown > 0); + action = e_attachment_view_get_action (view, "hide-all"); + gtk_action_set_visible (action, visible); + + action = e_attachment_view_get_action (view, "open-with"); + gtk_action_set_visible (action, !busy && n_selected == 1); + + action = e_attachment_view_get_action (view, "properties"); + gtk_action_set_visible (action, !busy && n_selected == 1); + + action = e_attachment_view_get_action (view, "remove"); + gtk_action_set_visible (action, !busy && n_selected > 0); + + action = e_attachment_view_get_action (view, "save-as"); + gtk_action_set_visible (action, !busy && n_selected > 0); + + action = e_attachment_view_get_action (view, "show"); + gtk_action_set_visible (action, can_show && !shown); + + /* Show this action if there are multiple viewable + * attachments, and at least one of them is hidden. */ + visible = (n_shown + n_hidden > 1) && (n_hidden > 0); + action = e_attachment_view_get_action (view, "show-all"); + gtk_action_set_visible (action, visible); + + /* Clear out the "openwith" action group. */ + gtk_ui_manager_remove_ui (priv->ui_manager, priv->merge_id); + action_group = e_attachment_view_get_action_group (view, "openwith"); + e_action_group_remove_all_actions (action_group); + gtk_ui_manager_ensure_update (priv->ui_manager); + + if (attachment == NULL || busy) + return; + + list = e_attachment_list_apps (attachment); + + for (iter = list; iter != NULL; iter = iter->next) { + GAppInfo *app_info = iter->data; + GtkAction *action; + GIcon *app_icon; + const gchar *app_executable; + const gchar *app_name; + gchar *action_tooltip; + gchar *action_label; + gchar *action_name; + + app_executable = g_app_info_get_executable (app_info); + app_icon = g_app_info_get_icon (app_info); + app_name = g_app_info_get_name (app_info); + + action_name = g_strdup_printf ("open-with-%s", app_executable); + action_label = g_strdup_printf (_("Open With \"%s\""), app_name); + + action_tooltip = g_strdup_printf ( + _("Open this attachment in %s"), app_name); + + action = gtk_action_new ( + action_name, action_label, action_tooltip, NULL); + + gtk_action_set_gicon (action, app_icon); + + g_object_set_data_full ( + G_OBJECT (action), + "app-info", g_object_ref (app_info), + (GDestroyNotify) g_object_unref); + + g_object_set_data_full ( + G_OBJECT (action), + "attachment", g_object_ref (attachment), + (GDestroyNotify) g_object_unref); + + g_signal_connect ( + action, "activate", + G_CALLBACK (action_open_with_app_info_cb), view); + + gtk_action_group_add_action (action_group, action); + + gtk_ui_manager_add_ui ( + priv->ui_manager, priv->merge_id, + "/context/open-actions", action_name, + action_name, GTK_UI_MANAGER_AUTO, FALSE); + + g_free (action_name); + g_free (action_label); + g_free (action_tooltip); + } + + g_object_unref (attachment); + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); +} + +static void +attachment_view_init_drag_dest (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + GtkTargetList *target_list; + + priv = e_attachment_view_get_private (view); + + target_list = gtk_target_list_new ( + target_table, G_N_ELEMENTS (target_table)); + + gtk_target_list_add_uri_targets (target_list, 0); + e_target_list_add_calendar_targets (target_list, 0); + e_target_list_add_directory_targets (target_list, 0); + + priv->target_list = target_list; + priv->drag_actions = GDK_ACTION_COPY; +} + +static void +e_attachment_view_default_init (EAttachmentViewInterface *interface) +{ + interface->update_actions = attachment_view_update_actions; + + g_object_interface_install_property ( + interface, + g_param_spec_boolean ( + "dragging", + "Dragging", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_interface_install_property ( + interface, + g_param_spec_boolean ( + "editable", + "Editable", + NULL, + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + signals[UPDATE_ACTIONS] = g_signal_new ( + "update-actions", + G_TYPE_FROM_INTERFACE (interface), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EAttachmentViewInterface, update_actions), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /* Register known handler types. */ + e_attachment_handler_image_get_type (); + e_attachment_handler_sendto_get_type (); +} + +void +e_attachment_view_init (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + GtkUIManager *ui_manager; + GtkActionGroup *action_group; + GError *error = NULL; + + priv = e_attachment_view_get_private (view); + + ui_manager = e_ui_manager_new (); + priv->merge_id = gtk_ui_manager_new_merge_id (ui_manager); + priv->ui_manager = ui_manager; + + action_group = e_attachment_view_add_action_group (view, "standard"); + + gtk_action_group_add_actions ( + action_group, standard_entries, + G_N_ELEMENTS (standard_entries), view); + + action_group = e_attachment_view_add_action_group (view, "editable"); + + g_object_bind_property ( + view, "editable", + action_group, "visible", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + gtk_action_group_add_actions ( + action_group, editable_entries, + G_N_ELEMENTS (editable_entries), view); + + action_group = e_attachment_view_add_action_group (view, "inline"); + + gtk_action_group_add_actions ( + action_group, inline_entries, + G_N_ELEMENTS (inline_entries), view); + gtk_action_group_set_visible (action_group, FALSE); + + e_attachment_view_add_action_group (view, "openwith"); + + /* Because we are loading from a hard-coded string, there is + * no chance of I/O errors. Failure here implies a malformed + * UI definition. Full stop. */ + gtk_ui_manager_add_ui_from_string (ui_manager, ui, -1, &error); + if (error != NULL) + g_error ("%s", error->message); + + attachment_view_init_drag_dest (view); + + e_attachment_view_drag_source_set (view); + + /* Connect built-in drag and drop handlers. */ + + g_signal_connect ( + view, "drag-data-received", + G_CALLBACK (attachment_view_netscape_url), NULL); + + g_signal_connect ( + view, "drag-data-received", + G_CALLBACK (attachment_view_text_calendar), NULL); + + g_signal_connect ( + view, "drag-data-received", + G_CALLBACK (attachment_view_text_x_vcard), NULL); + + g_signal_connect ( + view, "drag-data-received", + G_CALLBACK (attachment_view_uris), NULL); +} + +void +e_attachment_view_dispose (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + + priv = e_attachment_view_get_private (view); + + if (priv->target_list != NULL) { + gtk_target_list_unref (priv->target_list); + priv->target_list = NULL; + } + + if (priv->ui_manager != NULL) { + g_object_unref (priv->ui_manager); + priv->ui_manager = NULL; + } +} + +void +e_attachment_view_finalize (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + + priv = e_attachment_view_get_private (view); + + g_list_foreach (priv->event_list, (GFunc) gdk_event_free, NULL); + g_list_free (priv->event_list); + + g_list_foreach (priv->selected, (GFunc) g_object_unref, NULL); + g_list_free (priv->selected); +} + +EAttachmentViewPrivate * +e_attachment_view_get_private (EAttachmentView *view) +{ + EAttachmentViewInterface *interface; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + g_return_val_if_fail (interface->get_private != NULL, NULL); + + return interface->get_private (view); +} + +EAttachmentStore * +e_attachment_view_get_store (EAttachmentView *view) +{ + EAttachmentViewInterface *interface; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + g_return_val_if_fail (interface->get_store != NULL, NULL); + + return interface->get_store (view); +} + +gboolean +e_attachment_view_get_editable (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), FALSE); + + priv = e_attachment_view_get_private (view); + + return priv->editable; +} + +void +e_attachment_view_set_editable (EAttachmentView *view, + gboolean editable) +{ + EAttachmentViewPrivate *priv; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + priv = e_attachment_view_get_private (view); + + priv->editable = editable; + + if (editable) + e_attachment_view_drag_dest_set (view); + else + e_attachment_view_drag_dest_unset (view); + + g_object_notify (G_OBJECT (view), "editable"); +} + +gboolean +e_attachment_view_get_dragging (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), FALSE); + + priv = e_attachment_view_get_private (view); + + return priv->dragging; +} + +void +e_attachment_view_set_dragging (EAttachmentView *view, + gboolean dragging) +{ + EAttachmentViewPrivate *priv; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + priv = e_attachment_view_get_private (view); + + priv->dragging = dragging; + + g_object_notify (G_OBJECT (view), "dragging"); +} + +GtkTargetList * +e_attachment_view_get_target_list (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + + priv = e_attachment_view_get_private (view); + + return priv->target_list; +} + +GdkDragAction +e_attachment_view_get_drag_actions (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), 0); + + priv = e_attachment_view_get_private (view); + + return priv->drag_actions; +} + +void +e_attachment_view_add_drag_actions (EAttachmentView *view, + GdkDragAction drag_actions) +{ + EAttachmentViewPrivate *priv; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + priv = e_attachment_view_get_private (view); + + priv->drag_actions |= drag_actions; +} + +GList * +e_attachment_view_get_selected_attachments (EAttachmentView *view) +{ + EAttachmentStore *store; + GtkTreeModel *model; + GList *list, *item; + gint column_id; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + + column_id = E_ATTACHMENT_STORE_COLUMN_ATTACHMENT; + list = e_attachment_view_get_selected_paths (view); + store = e_attachment_view_get_store (view); + model = GTK_TREE_MODEL (store); + + /* Convert the GtkTreePaths to EAttachments. */ + for (item = list; item != NULL; item = item->next) { + EAttachment *attachment; + GtkTreePath *path; + GtkTreeIter iter; + + path = item->data; + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, column_id, &attachment, -1); + gtk_tree_path_free (path); + + item->data = attachment; + } + + return list; +} + +void +e_attachment_view_open_path (EAttachmentView *view, + GtkTreePath *path, + GAppInfo *app_info) +{ + EAttachmentStore *store; + EAttachment *attachment; + GtkTreeModel *model; + GtkTreeIter iter; + gpointer parent; + gint column_id; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + g_return_if_fail (path != NULL); + + column_id = E_ATTACHMENT_STORE_COLUMN_ATTACHMENT; + store = e_attachment_view_get_store (view); + model = GTK_TREE_MODEL (store); + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, column_id, &attachment, -1); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + e_attachment_open_async ( + attachment, app_info, (GAsyncReadyCallback) + e_attachment_open_handle_error, parent); + + g_object_unref (attachment); +} + +void +e_attachment_view_remove_selected (EAttachmentView *view, + gboolean select_next) +{ + EAttachmentStore *store; + GtkTreeModel *model; + GList *list, *item; + gint column_id; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + column_id = E_ATTACHMENT_STORE_COLUMN_ATTACHMENT; + list = e_attachment_view_get_selected_paths (view); + store = e_attachment_view_get_store (view); + model = GTK_TREE_MODEL (store); + + /* Remove attachments in reverse order to avoid invalidating + * tree paths as we iterate over the list. Note, the list is + * probably already sorted but we sort again just to be safe. */ + list = g_list_reverse (g_list_sort ( + list, (GCompareFunc) gtk_tree_path_compare)); + + for (item = list; item != NULL; item = item->next) { + EAttachment *attachment; + GtkTreePath *path = item->data; + GtkTreeIter iter; + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, column_id, &attachment, -1); + e_attachment_store_remove_attachment (store, attachment); + g_object_unref (attachment); + } + + /* If we only removed one attachment, try to select another. */ + if (select_next && g_list_length (list) == 1) { + GtkTreePath *path = list->data; + + e_attachment_view_select_path (view, path); + if (!e_attachment_view_path_is_selected (view, path)) + if (gtk_tree_path_prev (path)) + e_attachment_view_select_path (view, path); + } + + g_list_foreach (list, (GFunc) gtk_tree_path_free, NULL); + g_list_free (list); +} + +gboolean +e_attachment_view_button_press_event (EAttachmentView *view, + GdkEventButton *event) +{ + EAttachmentViewPrivate *priv; + GtkTreePath *path; + gboolean editable; + gboolean handled = FALSE; + gboolean path_is_selected = FALSE; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + priv = e_attachment_view_get_private (view); + + if (g_list_find (priv->event_list, event) != NULL) + return FALSE; + + if (priv->event_list != NULL) { + /* Save the event to be propagated in order. */ + priv->event_list = g_list_append ( + priv->event_list, + gdk_event_copy ((GdkEvent *) event)); + return TRUE; + } + + editable = e_attachment_view_get_editable (view); + path = e_attachment_view_get_path_at_pos (view, event->x, event->y); + path_is_selected = e_attachment_view_path_is_selected (view, path); + + if (event->button == 1 && event->type == GDK_BUTTON_PRESS) { + GList *list, *iter; + gboolean busy = FALSE; + + list = e_attachment_view_get_selected_attachments (view); + + for (iter = list; iter != NULL; iter = iter->next) { + EAttachment *attachment = iter->data; + busy |= e_attachment_get_loading (attachment); + busy |= e_attachment_get_saving (attachment); + } + + /* Prepare for dragging if the clicked item is selected + * and none of the selected items are loading or saving. */ + if (path_is_selected && !busy) { + priv->start_x = event->x; + priv->start_y = event->y; + priv->event_list = g_list_append ( + priv->event_list, + gdk_event_copy ((GdkEvent *) event)); + handled = TRUE; + } + + g_list_foreach (list, (GFunc) g_object_unref, NULL); + g_list_free (list); + } + + if (event->button == 3 && event->type == GDK_BUTTON_PRESS) { + /* If the user clicked on a selected item, retain the + * current selection. If the user clicked on an unselected + * item, select the clicked item only. If the user did not + * click on an item, clear the current selection. */ + if (path == NULL) + e_attachment_view_unselect_all (view); + else if (!path_is_selected) { + e_attachment_view_unselect_all (view); + e_attachment_view_select_path (view, path); + } + + /* Non-editable attachment views should only show a + * popup menu when right-clicking on an attachment, + * but editable views can show the menu any time. */ + if (path != NULL || editable) { + e_attachment_view_show_popup_menu ( + view, event, NULL, NULL); + handled = TRUE; + } + } + + if (path != NULL) + gtk_tree_path_free (path); + + return handled; +} + +gboolean +e_attachment_view_button_release_event (EAttachmentView *view, + GdkEventButton *event) +{ + EAttachmentViewPrivate *priv; + GtkWidget *widget = GTK_WIDGET (view); + GList *iter; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + priv = e_attachment_view_get_private (view); + + for (iter = priv->event_list; iter != NULL; iter = iter->next) { + GdkEvent *event = iter->data; + + gtk_propagate_event (widget, event); + gdk_event_free (event); + } + + g_list_free (priv->event_list); + priv->event_list = NULL; + + return FALSE; +} + +gboolean +e_attachment_view_motion_notify_event (EAttachmentView *view, + GdkEventMotion *event) +{ + EAttachmentViewPrivate *priv; + GtkWidget *widget = GTK_WIDGET (view); + GtkTargetList *targets; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + priv = e_attachment_view_get_private (view); + + if (priv->event_list == NULL) + return FALSE; + + if (!gtk_drag_check_threshold ( + widget, priv->start_x, priv->start_y, event->x, event->y)) + return TRUE; + + g_list_foreach (priv->event_list, (GFunc) gdk_event_free, NULL); + g_list_free (priv->event_list); + priv->event_list = NULL; + + targets = gtk_drag_source_get_target_list (widget); + + gtk_drag_begin ( + widget, targets, GDK_ACTION_COPY, 1, (GdkEvent *) event); + + return TRUE; +} + +gboolean +e_attachment_view_key_press_event (EAttachmentView *view, + GdkEventKey *event) +{ + gboolean editable; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + editable = e_attachment_view_get_editable (view); + + if (event->keyval == GDK_KEY_Delete && editable) { + e_attachment_view_remove_selected (view, TRUE); + return TRUE; + } + + return FALSE; +} + +GtkTreePath * +e_attachment_view_get_path_at_pos (EAttachmentView *view, + gint x, + gint y) +{ + EAttachmentViewInterface *interface; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + g_return_val_if_fail (interface->get_path_at_pos != NULL, NULL); + + return interface->get_path_at_pos (view, x, y); +} + +GList * +e_attachment_view_get_selected_paths (EAttachmentView *view) +{ + EAttachmentViewInterface *interface; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + g_return_val_if_fail (interface->get_selected_paths != NULL, NULL); + + return interface->get_selected_paths (view); +} + +gboolean +e_attachment_view_path_is_selected (EAttachmentView *view, + GtkTreePath *path) +{ + EAttachmentViewInterface *interface; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), FALSE); + + /* Handle NULL paths gracefully. */ + if (path == NULL) + return FALSE; + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + g_return_val_if_fail (interface->path_is_selected != NULL, FALSE); + + return interface->path_is_selected (view, path); +} + +void +e_attachment_view_select_path (EAttachmentView *view, + GtkTreePath *path) +{ + EAttachmentViewInterface *interface; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + g_return_if_fail (path != NULL); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + g_return_if_fail (interface->select_path != NULL); + + interface->select_path (view, path); +} + +void +e_attachment_view_unselect_path (EAttachmentView *view, + GtkTreePath *path) +{ + EAttachmentViewInterface *interface; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + g_return_if_fail (path != NULL); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + g_return_if_fail (interface->unselect_path != NULL); + + interface->unselect_path (view, path); +} + +void +e_attachment_view_select_all (EAttachmentView *view) +{ + EAttachmentViewInterface *interface; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + g_return_if_fail (interface->select_all != NULL); + + interface->select_all (view); +} + +void +e_attachment_view_unselect_all (EAttachmentView *view) +{ + EAttachmentViewInterface *interface; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + g_return_if_fail (interface->unselect_all != NULL); + + interface->unselect_all (view); +} + +void +e_attachment_view_sync_selection (EAttachmentView *view, + EAttachmentView *target) +{ + GList *list, *iter; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + g_return_if_fail (E_IS_ATTACHMENT_VIEW (target)); + + list = e_attachment_view_get_selected_paths (view); + e_attachment_view_unselect_all (target); + + for (iter = list; iter != NULL; iter = iter->next) + e_attachment_view_select_path (target, iter->data); + + g_list_foreach (list, (GFunc) gtk_tree_path_free, NULL); + g_list_free (list); +} + +void +e_attachment_view_drag_source_set (EAttachmentView *view) +{ + EAttachmentViewInterface *interface; + GtkTargetEntry *targets; + GtkTargetList *list; + gint n_targets; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + if (interface->drag_source_set == NULL) + return; + + list = gtk_target_list_new (NULL, 0); + gtk_target_list_add_uri_targets (list, 0); + targets = gtk_target_table_new_from_list (list, &n_targets); + + interface->drag_source_set ( + view, GDK_BUTTON1_MASK, + targets, n_targets, GDK_ACTION_COPY); + + gtk_target_table_free (targets, n_targets); + gtk_target_list_unref (list); +} + +void +e_attachment_view_drag_source_unset (EAttachmentView *view) +{ + EAttachmentViewInterface *interface; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + if (interface->drag_source_unset == NULL) + return; + + interface->drag_source_unset (view); +} + +void +e_attachment_view_drag_begin (EAttachmentView *view, + GdkDragContext *context) +{ + EAttachmentViewPrivate *priv; + guint n_selected; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + g_return_if_fail (GDK_IS_DRAG_CONTEXT (context)); + + priv = e_attachment_view_get_private (view); + + e_attachment_view_set_dragging (view, TRUE); + + g_warn_if_fail (priv->selected == NULL); + priv->selected = e_attachment_view_get_selected_attachments (view); + n_selected = g_list_length (priv->selected); + + if (n_selected > 1) + gtk_drag_set_icon_stock ( + context, GTK_STOCK_DND_MULTIPLE, 0, 0); + + else if (n_selected == 1) { + EAttachment *attachment; + GtkIconTheme *icon_theme; + GtkIconInfo *icon_info; + GIcon *icon; + gint width, height; + + attachment = E_ATTACHMENT (priv->selected->data); + icon = e_attachment_get_icon (attachment); + g_return_if_fail (icon != NULL); + + icon_theme = gtk_icon_theme_get_default (); + gtk_icon_size_lookup (GTK_ICON_SIZE_DND, &width, &height); + + icon_info = gtk_icon_theme_lookup_by_gicon ( + icon_theme, icon, MIN (width, height), + GTK_ICON_LOOKUP_USE_BUILTIN); + + if (icon_info != NULL) { + GdkPixbuf *pixbuf; + GError *error = NULL; + + pixbuf = gtk_icon_info_load_icon (icon_info, &error); + + if (pixbuf != NULL) { + gtk_drag_set_icon_pixbuf ( + context, pixbuf, 0, 0); + g_object_unref (pixbuf); + } else if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } + + gtk_icon_info_free (icon_info); + } + } +} + +void +e_attachment_view_drag_end (EAttachmentView *view, + GdkDragContext *context) +{ + EAttachmentViewPrivate *priv; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + g_return_if_fail (GDK_IS_DRAG_CONTEXT (context)); + + priv = e_attachment_view_get_private (view); + + e_attachment_view_set_dragging (view, FALSE); + + g_list_foreach (priv->selected, (GFunc) g_object_unref, NULL); + g_list_free (priv->selected); + priv->selected = NULL; +} + +static void +attachment_view_got_uris_cb (EAttachmentStore *store, + GAsyncResult *result, + gpointer user_data) +{ + struct { + gchar **uris; + gboolean done; + } *status = user_data; + + /* XXX Since this is a best-effort function, + * should we care about errors? */ + status->uris = e_attachment_store_get_uris_finish ( + store, result, NULL); + + status->done = TRUE; +} + +void +e_attachment_view_drag_data_get (EAttachmentView *view, + GdkDragContext *context, + GtkSelectionData *selection, + guint info, + guint time) +{ + EAttachmentViewPrivate *priv; + EAttachmentStore *store; + + struct { + gchar **uris; + gboolean done; + } status; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + g_return_if_fail (GDK_IS_DRAG_CONTEXT (context)); + g_return_if_fail (selection != NULL); + + status.uris = NULL; + status.done = FALSE; + + priv = e_attachment_view_get_private (view); + store = e_attachment_view_get_store (view); + + if (priv->selected == NULL) + return; + + e_attachment_store_get_uris_async ( + store, priv->selected, (GAsyncReadyCallback) + attachment_view_got_uris_cb, &status); + + /* We can't return until we have results, so crank + * the main loop until the callback gets triggered. */ + while (!status.done) + if (gtk_main_iteration ()) + break; + + if (status.uris != NULL) + gtk_selection_data_set_uris (selection, status.uris); + + g_strfreev (status.uris); +} + +void +e_attachment_view_drag_dest_set (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + EAttachmentViewInterface *interface; + GtkTargetEntry *targets; + gint n_targets; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + if (interface->drag_dest_set == NULL) + return; + + priv = e_attachment_view_get_private (view); + + targets = gtk_target_table_new_from_list ( + priv->target_list, &n_targets); + + interface->drag_dest_set ( + view, targets, n_targets, priv->drag_actions); + + gtk_target_table_free (targets, n_targets); +} + +void +e_attachment_view_drag_dest_unset (EAttachmentView *view) +{ + EAttachmentViewInterface *interface; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + interface = E_ATTACHMENT_VIEW_GET_INTERFACE (view); + if (interface->drag_dest_unset == NULL) + return; + + interface->drag_dest_unset (view); +} + +gboolean +e_attachment_view_drag_motion (EAttachmentView *view, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + EAttachmentViewPrivate *priv; + GdkDragAction actions; + GdkDragAction chosen_action; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), FALSE); + g_return_val_if_fail (GDK_IS_DRAG_CONTEXT (context), FALSE); + + priv = e_attachment_view_get_private (view); + + /* Disallow drops if we're not editable. */ + if (!e_attachment_view_get_editable (view)) + return FALSE; + + /* Disallow drops if we initiated the drag. + * This helps prevent duplicate attachments. */ + if (e_attachment_view_get_dragging (view)) + return FALSE; + + actions = gdk_drag_context_get_actions (context); + actions &= priv->drag_actions; + chosen_action = gdk_drag_context_get_suggested_action (context); + + if (chosen_action == GDK_ACTION_ASK) { + GdkDragAction mask; + + mask = GDK_ACTION_COPY | GDK_ACTION_MOVE; + if ((actions & mask) != mask) + chosen_action = GDK_ACTION_COPY; + } + + gdk_drag_status (context, chosen_action, time); + + return (chosen_action != 0); +} + +gboolean +e_attachment_view_drag_drop (EAttachmentView *view, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), FALSE); + g_return_val_if_fail (GDK_IS_DRAG_CONTEXT (context), FALSE); + + /* Disallow drops if we initiated the drag. + * This helps prevent duplicate attachments. */ + return !e_attachment_view_get_dragging (view); +} + +void +e_attachment_view_drag_data_received (EAttachmentView *view, + GdkDragContext *drag_context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time) +{ + GdkAtom atom; + gchar *name; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + g_return_if_fail (GDK_IS_DRAG_CONTEXT (drag_context)); + + /* Drop handlers are supposed to stop further emission of the + * "drag-data-received" signal if they can handle the data. If + * we get this far it means none of the handlers were successful, + * so report the drop as failed. */ + + atom = gtk_selection_data_get_target (selection_data); + + name = gdk_atom_name (atom); + g_warning ("Unknown selection target: %s", name); + g_free (name); + + gtk_drag_finish (drag_context, FALSE, FALSE, time); +} + +GtkAction * +e_attachment_view_get_action (EAttachmentView *view, + const gchar *action_name) +{ + GtkUIManager *ui_manager; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + g_return_val_if_fail (action_name != NULL, NULL); + + ui_manager = e_attachment_view_get_ui_manager (view); + + return e_lookup_action (ui_manager, action_name); +} + +GtkActionGroup * +e_attachment_view_add_action_group (EAttachmentView *view, + const gchar *group_name) +{ + GtkActionGroup *action_group; + GtkUIManager *ui_manager; + const gchar *domain; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + g_return_val_if_fail (group_name != NULL, NULL); + + ui_manager = e_attachment_view_get_ui_manager (view); + domain = GETTEXT_PACKAGE; + + action_group = gtk_action_group_new (group_name); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + return action_group; +} + +GtkActionGroup * +e_attachment_view_get_action_group (EAttachmentView *view, + const gchar *group_name) +{ + GtkUIManager *ui_manager; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + g_return_val_if_fail (group_name != NULL, NULL); + + ui_manager = e_attachment_view_get_ui_manager (view); + + return e_lookup_action_group (ui_manager, group_name); +} + +GtkWidget * +e_attachment_view_get_popup_menu (EAttachmentView *view) +{ + GtkUIManager *ui_manager; + GtkWidget *menu; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + + ui_manager = e_attachment_view_get_ui_manager (view); + menu = gtk_ui_manager_get_widget (ui_manager, "/context"); + g_return_val_if_fail (GTK_IS_MENU (menu), NULL); + + return menu; +} + +GtkUIManager * +e_attachment_view_get_ui_manager (EAttachmentView *view) +{ + EAttachmentViewPrivate *priv; + + g_return_val_if_fail (E_IS_ATTACHMENT_VIEW (view), NULL); + + priv = e_attachment_view_get_private (view); + + return priv->ui_manager; +} + +void +e_attachment_view_show_popup_menu (EAttachmentView *view, + GdkEventButton *event, + GtkMenuPositionFunc func, + gpointer user_data) +{ + GtkWidget *menu; + + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + e_attachment_view_update_actions (view); + + menu = e_attachment_view_get_popup_menu (view); + + if (event != NULL) + gtk_menu_popup ( + GTK_MENU (menu), NULL, NULL, func, + user_data, event->button, event->time); + else + gtk_menu_popup ( + GTK_MENU (menu), NULL, NULL, func, + user_data, 0, gtk_get_current_event_time ()); +} + +void +e_attachment_view_update_actions (EAttachmentView *view) +{ + g_return_if_fail (E_IS_ATTACHMENT_VIEW (view)); + + g_signal_emit (view, signals[UPDATE_ACTIONS], 0); +} diff --git a/e-util/e-attachment-view.h b/e-util/e-attachment-view.h new file mode 100644 index 0000000000..174181541a --- /dev/null +++ b/e-util/e-attachment-view.h @@ -0,0 +1,244 @@ +/* + * e-attachment-view.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_VIEW_H +#define E_ATTACHMENT_VIEW_H + +#include <gtk/gtk.h> +#include <e-util/e-attachment-store.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT_VIEW \ + (e_attachment_view_get_type ()) +#define E_ATTACHMENT_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT_VIEW, EAttachmentView)) +#define E_ATTACHMENT_VIEW_INTERFACE(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT_VIEW, EAttachmentViewInterface)) +#define E_IS_ATTACHMENT_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT_VIEW)) +#define E_IS_ATTACHMENT_VIEW_INTERFACE(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT_VIEW)) +#define E_ATTACHMENT_VIEW_GET_INTERFACE(obj) \ + (G_TYPE_INSTANCE_GET_INTERFACE \ + ((obj), E_TYPE_ATTACHMENT_VIEW, EAttachmentViewInterface)) + +G_BEGIN_DECLS + +typedef struct _EAttachmentView EAttachmentView; +typedef struct _EAttachmentViewInterface EAttachmentViewInterface; +typedef struct _EAttachmentViewPrivate EAttachmentViewPrivate; + +struct _EAttachmentViewInterface { + GTypeInterface parent_interface; + + /* General Methods */ + EAttachmentViewPrivate * + (*get_private) (EAttachmentView *view); + EAttachmentStore * + (*get_store) (EAttachmentView *view); + + /* Selection Methods */ + GtkTreePath * (*get_path_at_pos) (EAttachmentView *view, + gint x, + gint y); + GList * (*get_selected_paths) (EAttachmentView *view); + gboolean (*path_is_selected) (EAttachmentView *view, + GtkTreePath *path); + void (*select_path) (EAttachmentView *view, + GtkTreePath *path); + void (*unselect_path) (EAttachmentView *view, + GtkTreePath *path); + void (*select_all) (EAttachmentView *view); + void (*unselect_all) (EAttachmentView *view); + + /* Drag and Drop Methods */ + void (*drag_source_set) (EAttachmentView *view, + GdkModifierType start_button_mask, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions); + void (*drag_dest_set) (EAttachmentView *view, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions); + void (*drag_source_unset) (EAttachmentView *view); + void (*drag_dest_unset) (EAttachmentView *view); + + /* Signals */ + void (*update_actions) (EAttachmentView *view); +}; + +struct _EAttachmentViewPrivate { + + /* Drag Destination */ + GtkTargetList *target_list; + GdkDragAction drag_actions; + + /* Popup Menu Management */ + GtkUIManager *ui_manager; + guint merge_id; + + /* Multi-DnD State */ + GList *event_list; + GList *selected; + gint start_x; + gint start_y; + + guint dragging : 1; + guint editable : 1; +}; + +GType e_attachment_view_get_type (void); + +void e_attachment_view_init (EAttachmentView *view); +void e_attachment_view_dispose (EAttachmentView *view); +void e_attachment_view_finalize (EAttachmentView *view); + +EAttachmentViewPrivate * + e_attachment_view_get_private (EAttachmentView *view); +EAttachmentStore * + e_attachment_view_get_store (EAttachmentView *view); +gboolean e_attachment_view_get_dragging (EAttachmentView *view); +void e_attachment_view_set_dragging (EAttachmentView *view, + gboolean dragging); +gboolean e_attachment_view_get_editable (EAttachmentView *view); +void e_attachment_view_set_editable (EAttachmentView *view, + gboolean editable); +GtkTargetList * e_attachment_view_get_target_list + (EAttachmentView *view); +GdkDragAction e_attachment_view_get_drag_actions + (EAttachmentView *view); +void e_attachment_view_add_drag_actions + (EAttachmentView *view, + GdkDragAction drag_actions); +GList * e_attachment_view_get_selected_attachments + (EAttachmentView *view); +void e_attachment_view_open_path (EAttachmentView *view, + GtkTreePath *path, + GAppInfo *app_info); +void e_attachment_view_remove_selected + (EAttachmentView *view, + gboolean select_next); + +/* Event Support */ +gboolean e_attachment_view_button_press_event + (EAttachmentView *view, + GdkEventButton *event); +gboolean e_attachment_view_button_release_event + (EAttachmentView *view, + GdkEventButton *event); +gboolean e_attachment_view_motion_notify_event + (EAttachmentView *view, + GdkEventMotion *event); +gboolean e_attachment_view_key_press_event + (EAttachmentView *view, + GdkEventKey *event); + +/* Selection Management */ +GtkTreePath * e_attachment_view_get_path_at_pos + (EAttachmentView *view, + gint x, + gint y); +GList * e_attachment_view_get_selected_paths + (EAttachmentView *view); +gboolean e_attachment_view_path_is_selected + (EAttachmentView *view, + GtkTreePath *path); +void e_attachment_view_select_path (EAttachmentView *view, + GtkTreePath *path); +void e_attachment_view_unselect_path (EAttachmentView *view, + GtkTreePath *path); +void e_attachment_view_select_all (EAttachmentView *view); +void e_attachment_view_unselect_all (EAttachmentView *view); +void e_attachment_view_sync_selection + (EAttachmentView *view, + EAttachmentView *target); + +/* Drag Source Support */ +void e_attachment_view_drag_source_set + (EAttachmentView *view); +void e_attachment_view_drag_source_unset + (EAttachmentView *view); +void e_attachment_view_drag_begin (EAttachmentView *view, + GdkDragContext *context); +void e_attachment_view_drag_end (EAttachmentView *view, + GdkDragContext *context); +void e_attachment_view_drag_data_get (EAttachmentView *view, + GdkDragContext *context, + GtkSelectionData *selection, + guint info, + guint time); + +/* Drag Destination Support */ +void e_attachment_view_drag_dest_set (EAttachmentView *view); +void e_attachment_view_drag_dest_unset + (EAttachmentView *view); +gboolean e_attachment_view_drag_motion (EAttachmentView *view, + GdkDragContext *context, + gint x, + gint y, + guint time); +gboolean e_attachment_view_drag_drop (EAttachmentView *view, + GdkDragContext *context, + gint x, + gint y, + guint time); +void e_attachment_view_drag_data_received + (EAttachmentView *view, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection, + guint info, + guint time); + +/* Popup Menu Management */ +GtkAction * e_attachment_view_get_action (EAttachmentView *view, + const gchar *action_name); +GtkActionGroup *e_attachment_view_add_action_group + (EAttachmentView *view, + const gchar *group_name); +GtkActionGroup *e_attachment_view_get_action_group + (EAttachmentView *view, + const gchar *group_name); +GtkWidget * e_attachment_view_get_popup_menu + (EAttachmentView *view); +GtkUIManager * e_attachment_view_get_ui_manager + (EAttachmentView *view); +void e_attachment_view_show_popup_menu + (EAttachmentView *view, + GdkEventButton *event, + GtkMenuPositionFunc func, + gpointer user_data); +void e_attachment_view_update_actions + (EAttachmentView *view); + +G_END_DECLS + +#endif /* E_ATTACHMENT_VIEW_H */ diff --git a/e-util/e-attachment.c b/e-util/e-attachment.c new file mode 100644 index 0000000000..3357775111 --- /dev/null +++ b/e-util/e-attachment.c @@ -0,0 +1,2882 @@ +/* + * e-attachment.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-attachment.h" + +#include <errno.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> + +#include <libedataserver/libedataserver.h> + +#include "e-attachment-store.h" +#include "e-icon-factory.h" +#include "e-mktemp.h" + +#define E_ATTACHMENT_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ATTACHMENT, EAttachmentPrivate)) + +/* Fallback Icon */ +#define DEFAULT_ICON_NAME "mail-attachment" + +/* Emblems */ +#define EMBLEM_CANCELLED "gtk-cancel" +#define EMBLEM_LOADING "emblem-downloads" +#define EMBLEM_SAVING "document-save" +#define EMBLEM_ENCRYPT_WEAK "security-low" +#define EMBLEM_ENCRYPT_STRONG "security-high" +#define EMBLEM_ENCRYPT_UNKNOWN "security-medium" +#define EMBLEM_SIGN_BAD "stock_signature-bad" +#define EMBLEM_SIGN_GOOD "stock_signature-ok" +#define EMBLEM_SIGN_UNKNOWN "stock_signature" + +/* Attributes needed for EAttachmentStore columns. */ +#define ATTACHMENT_QUERY "standard::*,preview::*,thumbnail::*" + +struct _EAttachmentPrivate { + GFile *file; + GIcon *icon; + GFileInfo *file_info; + GCancellable *cancellable; + CamelMimePart *mime_part; + guint emblem_timeout_id; + gchar *disposition; + gint percent; + gint64 last_percent_notify; /* to avoid excessive notifications */ + + guint can_show : 1; + guint loading : 1; + guint saving : 1; + guint shown : 1; + + camel_cipher_validity_encrypt_t encrypted; + camel_cipher_validity_sign_t signed_; + + /* This is a reference to our row in an EAttachmentStore, + * serving as a means of broadcasting "row-changed" signals. + * If we are removed from the store, we lazily free the + * reference when it is found to be to be invalid. */ + GtkTreeRowReference *reference; +}; + +enum { + PROP_0, + PROP_CAN_SHOW, + PROP_DISPOSITION, + PROP_ENCRYPTED, + PROP_FILE, + PROP_FILE_INFO, + PROP_ICON, + PROP_LOADING, + PROP_MIME_PART, + PROP_PERCENT, + PROP_REFERENCE, + PROP_SAVING, + PROP_SHOWN, + PROP_SIGNED +}; + +G_DEFINE_TYPE ( + EAttachment, + e_attachment, + G_TYPE_OBJECT) + +static gboolean +create_system_thumbnail (EAttachment *attachment, + GIcon **icon) +{ + GFile *file; + GFile *icon_file; + gchar *thumbnail = NULL; + + g_return_val_if_fail (attachment != NULL, FALSE); + g_return_val_if_fail (icon != NULL, FALSE); + + file = e_attachment_get_file (attachment); + + if (file && g_file_has_uri_scheme (file, "file")) { + gchar *path = g_file_get_path (file); + if (path) { + thumbnail = e_icon_factory_create_thumbnail (path); + g_free (path); + } + } + + if (thumbnail == NULL) + return FALSE; + + icon_file = g_file_new_for_path (thumbnail); + + if (*icon) + g_object_unref (*icon); + + *icon = g_file_icon_new (icon_file); + + g_object_unref (icon_file); + + if (file) { + GFileInfo *file_info; + const gchar *attribute; + + file_info = e_attachment_get_file_info (attachment); + attribute = G_FILE_ATTRIBUTE_THUMBNAIL_PATH; + + if (file_info != NULL) + g_file_info_set_attribute_byte_string ( + file_info, attribute, thumbnail); + } + + g_free (thumbnail); + + return TRUE; +} + +static gchar * +attachment_get_default_charset (void) +{ + GSettings *settings; + gchar *charset; + + /* XXX This doesn't really belong here. */ + + settings = g_settings_new ("org.gnome.evolution.mail"); + charset = g_settings_get_string (settings, "composer-charset"); + if (charset == NULL || *charset == '\0') { + g_free (charset); + /* FIXME This was "/apps/evolution/mail/format/charset", + * not sure it relates to "charset" */ + charset = g_settings_get_string (settings, "charset"); + if (charset == NULL || *charset == '\0') { + g_free (charset); + charset = NULL; + } + } + g_object_unref (settings); + + if (charset == NULL) + charset = g_strdup (camel_iconv_locale_charset ()); + + if (charset == NULL) + charset = g_strdup ("us-ascii"); + + return charset; +} + +static void +attachment_update_file_info_columns (EAttachment *attachment) +{ + GtkTreeRowReference *reference; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + GFileInfo *file_info; + const gchar *content_type; + const gchar *description; + const gchar *display_name; + gchar *content_desc; + gchar *display_size; + gchar *caption; + goffset size; + + reference = e_attachment_get_reference (attachment); + if (!gtk_tree_row_reference_valid (reference)) + return; + + file_info = e_attachment_get_file_info (attachment); + if (file_info == NULL) + return; + + model = gtk_tree_row_reference_get_model (reference); + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + + content_type = g_file_info_get_content_type (file_info); + display_name = g_file_info_get_display_name (file_info); + size = g_file_info_get_size (file_info); + + content_desc = g_content_type_get_description (content_type); + display_size = g_format_size (size); + + description = e_attachment_get_description (attachment); + if (description == NULL || *description == '\0') + description = display_name; + + if (size > 0) + caption = g_strdup_printf ( + "%s\n(%s)", description, display_size); + else + caption = g_strdup (description); + + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + E_ATTACHMENT_STORE_COLUMN_CAPTION, caption, + E_ATTACHMENT_STORE_COLUMN_CONTENT_TYPE, content_desc, + E_ATTACHMENT_STORE_COLUMN_DESCRIPTION, description, + E_ATTACHMENT_STORE_COLUMN_SIZE, size, + -1); + + g_free (content_desc); + g_free (display_size); + g_free (caption); +} + +static void +attachment_update_icon_column (EAttachment *attachment) +{ + GtkTreeRowReference *reference; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + GFileInfo *file_info; + GCancellable *cancellable; + GIcon *icon = NULL; + const gchar *emblem_name = NULL; + const gchar *thumbnail_path = NULL; + + reference = e_attachment_get_reference (attachment); + if (!gtk_tree_row_reference_valid (reference)) + return; + + model = gtk_tree_row_reference_get_model (reference); + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + + cancellable = attachment->priv->cancellable; + file_info = e_attachment_get_file_info (attachment); + + if (file_info != NULL) { + icon = g_file_info_get_icon (file_info); + thumbnail_path = g_file_info_get_attribute_byte_string ( + file_info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + } + + /* Prefer the thumbnail if we have one. */ + if (thumbnail_path != NULL && *thumbnail_path != '\0') { + GFile *file; + + file = g_file_new_for_path (thumbnail_path); + icon = g_file_icon_new (file); + g_object_unref (file); + + /* Try the system thumbnailer. */ + } else if (create_system_thumbnail (attachment, &icon)) { + /* Nothing to do, just use the icon. */ + + /* Else use the standard icon for the content type. */ + } else if (icon != NULL) + g_object_ref (icon); + + /* Last ditch fallback. (GFileInfo not yet loaded?) */ + else + icon = g_themed_icon_new (DEFAULT_ICON_NAME); + + /* Pick an emblem, limit one. Choices listed by priority. */ + + if (g_cancellable_is_cancelled (cancellable)) + emblem_name = EMBLEM_CANCELLED; + + else if (e_attachment_get_loading (attachment)) + emblem_name = EMBLEM_LOADING; + + else if (e_attachment_get_saving (attachment)) + emblem_name = EMBLEM_SAVING; + + else if (e_attachment_get_encrypted (attachment)) + switch (e_attachment_get_encrypted (attachment)) { + case CAMEL_CIPHER_VALIDITY_ENCRYPT_WEAK: + emblem_name = EMBLEM_ENCRYPT_WEAK; + break; + + case CAMEL_CIPHER_VALIDITY_ENCRYPT_ENCRYPTED: + emblem_name = EMBLEM_ENCRYPT_UNKNOWN; + break; + + case CAMEL_CIPHER_VALIDITY_ENCRYPT_STRONG: + emblem_name = EMBLEM_ENCRYPT_STRONG; + break; + + default: + g_warn_if_reached (); + break; + } + + else if (e_attachment_get_signed (attachment)) + switch (e_attachment_get_signed (attachment)) { + case CAMEL_CIPHER_VALIDITY_SIGN_GOOD: + emblem_name = EMBLEM_SIGN_GOOD; + break; + + case CAMEL_CIPHER_VALIDITY_SIGN_BAD: + emblem_name = EMBLEM_SIGN_BAD; + break; + + case CAMEL_CIPHER_VALIDITY_SIGN_UNKNOWN: + case CAMEL_CIPHER_VALIDITY_SIGN_NEED_PUBLIC_KEY: + emblem_name = EMBLEM_SIGN_UNKNOWN; + break; + + default: + g_warn_if_reached (); + break; + } + + if (emblem_name != NULL) { + GIcon *emblemed_icon; + GEmblem *emblem; + + emblemed_icon = g_themed_icon_new (emblem_name); + emblem = g_emblem_new (emblemed_icon); + g_object_unref (emblemed_icon); + + emblemed_icon = g_emblemed_icon_new (icon, emblem); + g_object_unref (emblem); + g_object_unref (icon); + + icon = emblemed_icon; + } + + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + E_ATTACHMENT_STORE_COLUMN_ICON, icon, + -1); + + /* Cache the icon to reuse for things like drag-n-drop. */ + if (attachment->priv->icon != NULL) + g_object_unref (attachment->priv->icon); + attachment->priv->icon = icon; + g_object_notify (G_OBJECT (attachment), "icon"); +} + +static void +attachment_update_progress_columns (EAttachment *attachment) +{ + GtkTreeRowReference *reference; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + gboolean loading; + gboolean saving; + gint percent; + + reference = e_attachment_get_reference (attachment); + if (!gtk_tree_row_reference_valid (reference)) + return; + + model = gtk_tree_row_reference_get_model (reference); + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + + /* Don't show progress bars until we have progress to report. */ + percent = e_attachment_get_percent (attachment); + loading = e_attachment_get_loading (attachment) && (percent > 0); + saving = e_attachment_get_saving (attachment) && (percent > 0); + + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + E_ATTACHMENT_STORE_COLUMN_LOADING, loading, + E_ATTACHMENT_STORE_COLUMN_PERCENT, percent, + E_ATTACHMENT_STORE_COLUMN_SAVING, saving, + -1); +} + +static void +attachment_set_loading (EAttachment *attachment, + gboolean loading) +{ + GtkTreeRowReference *reference; + + reference = e_attachment_get_reference (attachment); + + attachment->priv->percent = 0; + attachment->priv->loading = loading; + attachment->priv->last_percent_notify = 0; + + g_object_freeze_notify (G_OBJECT (attachment)); + g_object_notify (G_OBJECT (attachment), "percent"); + g_object_notify (G_OBJECT (attachment), "loading"); + g_object_thaw_notify (G_OBJECT (attachment)); + + if (gtk_tree_row_reference_valid (reference)) { + GtkTreeModel *model; + model = gtk_tree_row_reference_get_model (reference); + g_object_notify (G_OBJECT (model), "num-loading"); + } +} + +static void +attachment_set_saving (EAttachment *attachment, + gboolean saving) +{ + attachment->priv->percent = 0; + attachment->priv->saving = saving; + attachment->priv->last_percent_notify = 0; + + g_object_freeze_notify (G_OBJECT (attachment)); + g_object_notify (G_OBJECT (attachment), "percent"); + g_object_notify (G_OBJECT (attachment), "saving"); + g_object_thaw_notify (G_OBJECT (attachment)); +} + +static void +attachment_progress_cb (goffset current_num_bytes, + goffset total_num_bytes, + EAttachment *attachment) +{ + gint new_percent; + + /* Avoid dividing by zero. */ + if (total_num_bytes == 0) + return; + + /* do not notify too often, 5 times per second is sufficient */ + if (g_get_monotonic_time () - attachment->priv->last_percent_notify < 200000) + return; + + attachment->priv->last_percent_notify = g_get_monotonic_time (); + + new_percent = (current_num_bytes * 100) / total_num_bytes; + + if (new_percent != attachment->priv->percent) { + attachment->priv->percent = new_percent; + g_object_notify (G_OBJECT (attachment), "percent"); + } +} + +static gboolean +attachment_cancelled_timeout_cb (EAttachment *attachment) +{ + attachment->priv->emblem_timeout_id = 0; + g_cancellable_reset (attachment->priv->cancellable); + + attachment_update_icon_column (attachment); + + return FALSE; +} + +static void +attachment_cancelled_cb (EAttachment *attachment) +{ + /* Reset the GCancellable after one second. This causes a + * cancel emblem to be briefly shown on the attachment icon + * as visual feedback that an operation was cancelled. */ + + if (attachment->priv->emblem_timeout_id > 0) + g_source_remove (attachment->priv->emblem_timeout_id); + + attachment->priv->emblem_timeout_id = g_timeout_add_seconds ( + 1, (GSourceFunc) attachment_cancelled_timeout_cb, attachment); + + attachment_update_icon_column (attachment); +} + +static void +attachment_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CAN_SHOW: + e_attachment_set_can_show ( + E_ATTACHMENT (object), + g_value_get_boolean (value)); + return; + + case PROP_DISPOSITION: + e_attachment_set_disposition ( + E_ATTACHMENT (object), + g_value_get_string (value)); + return; + + case PROP_ENCRYPTED: + e_attachment_set_encrypted ( + E_ATTACHMENT (object), + g_value_get_int (value)); + return; + + case PROP_FILE: + e_attachment_set_file ( + E_ATTACHMENT (object), + g_value_get_object (value)); + return; + + case PROP_SHOWN: + e_attachment_set_shown ( + E_ATTACHMENT (object), + g_value_get_boolean (value)); + return; + + case PROP_MIME_PART: + e_attachment_set_mime_part ( + E_ATTACHMENT (object), + g_value_get_boxed (value)); + return; + + case PROP_REFERENCE: + e_attachment_set_reference ( + E_ATTACHMENT (object), + g_value_get_boxed (value)); + return; + + case PROP_SIGNED: + e_attachment_set_signed ( + E_ATTACHMENT (object), + g_value_get_int (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CAN_SHOW: + g_value_set_boolean ( + value, e_attachment_get_can_show ( + E_ATTACHMENT (object))); + return; + + case PROP_DISPOSITION: + g_value_set_string ( + value, e_attachment_get_disposition ( + E_ATTACHMENT (object))); + return; + + case PROP_ENCRYPTED: + g_value_set_int ( + value, e_attachment_get_encrypted ( + E_ATTACHMENT (object))); + return; + + case PROP_FILE: + g_value_set_object ( + value, e_attachment_get_file ( + E_ATTACHMENT (object))); + return; + + case PROP_FILE_INFO: + g_value_set_object ( + value, e_attachment_get_file_info ( + E_ATTACHMENT (object))); + return; + + case PROP_ICON: + g_value_set_object ( + value, e_attachment_get_icon ( + E_ATTACHMENT (object))); + return; + + case PROP_SHOWN: + g_value_set_boolean ( + value, e_attachment_get_shown ( + E_ATTACHMENT (object))); + return; + + case PROP_LOADING: + g_value_set_boolean ( + value, e_attachment_get_loading ( + E_ATTACHMENT (object))); + return; + + case PROP_MIME_PART: + g_value_set_boxed ( + value, e_attachment_get_mime_part ( + E_ATTACHMENT (object))); + return; + + case PROP_PERCENT: + g_value_set_int ( + value, e_attachment_get_percent ( + E_ATTACHMENT (object))); + return; + + case PROP_REFERENCE: + g_value_set_boxed ( + value, e_attachment_get_reference ( + E_ATTACHMENT (object))); + return; + + case PROP_SAVING: + g_value_set_boolean ( + value, e_attachment_get_saving ( + E_ATTACHMENT (object))); + return; + + case PROP_SIGNED: + g_value_set_int ( + value, e_attachment_get_signed ( + E_ATTACHMENT (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +attachment_dispose (GObject *object) +{ + EAttachmentPrivate *priv; + + priv = E_ATTACHMENT_GET_PRIVATE (object); + + if (priv->file != NULL) { + g_object_unref (priv->file); + priv->file = NULL; + } + + if (priv->icon != NULL) { + g_object_unref (priv->icon); + priv->icon = NULL; + } + + if (priv->file_info != NULL) { + g_object_unref (priv->file_info); + priv->file_info = NULL; + } + + if (priv->cancellable != NULL) { + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + if (priv->mime_part != NULL) { + g_object_unref (priv->mime_part); + priv->mime_part = NULL; + } + + if (priv->emblem_timeout_id > 0) { + g_source_remove (priv->emblem_timeout_id); + priv->emblem_timeout_id = 0; + } + + /* This accepts NULL arguments. */ + gtk_tree_row_reference_free (priv->reference); + priv->reference = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_attachment_parent_class)->dispose (object); +} + +static void +attachment_finalize (GObject *object) +{ + EAttachmentPrivate *priv; + + priv = E_ATTACHMENT_GET_PRIVATE (object); + + g_free (priv->disposition); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_attachment_parent_class)->finalize (object); +} + +static void +e_attachment_class_init (EAttachmentClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EAttachmentPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = attachment_set_property; + object_class->get_property = attachment_get_property; + object_class->dispose = attachment_dispose; + object_class->finalize = attachment_finalize; + + g_object_class_install_property ( + object_class, + PROP_CAN_SHOW, + g_param_spec_boolean ( + "can-show", + "Can Show", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_DISPOSITION, + g_param_spec_string ( + "disposition", + "Disposition", + NULL, + "attachment", + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + /* FIXME Define a GEnumClass for this. */ + g_object_class_install_property ( + object_class, + PROP_ENCRYPTED, + g_param_spec_int ( + "encrypted", + "Encrypted", + NULL, + CAMEL_CIPHER_VALIDITY_ENCRYPT_NONE, + CAMEL_CIPHER_VALIDITY_ENCRYPT_STRONG, + CAMEL_CIPHER_VALIDITY_ENCRYPT_NONE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_FILE, + g_param_spec_object ( + "file", + "File", + NULL, + G_TYPE_FILE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_FILE_INFO, + g_param_spec_object ( + "file-info", + "File Info", + NULL, + G_TYPE_FILE_INFO, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_ICON, + g_param_spec_object ( + "icon", + "Icon", + NULL, + G_TYPE_ICON, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_LOADING, + g_param_spec_boolean ( + "loading", + "Loading", + NULL, + FALSE, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_MIME_PART, + g_param_spec_object ( + "mime-part", + "MIME Part", + NULL, + CAMEL_TYPE_MIME_PART, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_PERCENT, + g_param_spec_int ( + "percent", + "Percent", + NULL, + 0, + 100, + 0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_REFERENCE, + g_param_spec_boxed ( + "reference", + "Reference", + NULL, + GTK_TYPE_TREE_ROW_REFERENCE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SAVING, + g_param_spec_boolean ( + "saving", + "Saving", + NULL, + FALSE, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_SHOWN, + g_param_spec_boolean ( + "shown", + "Shown", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + /* FIXME Define a GEnumClass for this. */ + g_object_class_install_property ( + object_class, + PROP_SIGNED, + g_param_spec_int ( + "signed", + "Signed", + NULL, + CAMEL_CIPHER_VALIDITY_SIGN_NONE, + CAMEL_CIPHER_VALIDITY_SIGN_NEED_PUBLIC_KEY, + CAMEL_CIPHER_VALIDITY_SIGN_NONE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); +} + +static void +e_attachment_init (EAttachment *attachment) +{ + attachment->priv = E_ATTACHMENT_GET_PRIVATE (attachment); + attachment->priv->cancellable = g_cancellable_new (); + attachment->priv->encrypted = CAMEL_CIPHER_VALIDITY_ENCRYPT_NONE; + attachment->priv->signed_ = CAMEL_CIPHER_VALIDITY_SIGN_NONE; + + g_signal_connect ( + attachment, "notify::encrypted", + G_CALLBACK (attachment_update_icon_column), NULL); + + g_signal_connect ( + attachment, "notify::file-info", + G_CALLBACK (attachment_update_file_info_columns), NULL); + + g_signal_connect ( + attachment, "notify::file-info", + G_CALLBACK (attachment_update_icon_column), NULL); + + g_signal_connect ( + attachment, "notify::loading", + G_CALLBACK (attachment_update_icon_column), NULL); + + g_signal_connect ( + attachment, "notify::loading", + G_CALLBACK (attachment_update_progress_columns), NULL); + + g_signal_connect ( + attachment, "notify::percent", + G_CALLBACK (attachment_update_progress_columns), NULL); + + g_signal_connect ( + attachment, "notify::reference", + G_CALLBACK (attachment_update_file_info_columns), NULL); + + g_signal_connect ( + attachment, "notify::reference", + G_CALLBACK (attachment_update_icon_column), NULL); + + g_signal_connect ( + attachment, "notify::reference", + G_CALLBACK (attachment_update_progress_columns), NULL); + + g_signal_connect ( + attachment, "notify::saving", + G_CALLBACK (attachment_update_icon_column), NULL); + + g_signal_connect ( + attachment, "notify::saving", + G_CALLBACK (attachment_update_progress_columns), NULL); + + g_signal_connect ( + attachment, "notify::signed", + G_CALLBACK (attachment_update_icon_column), NULL); + + g_signal_connect_swapped ( + attachment->priv->cancellable, "cancelled", + G_CALLBACK (attachment_cancelled_cb), attachment); +} + +EAttachment * +e_attachment_new (void) +{ + return g_object_new (E_TYPE_ATTACHMENT, NULL); +} + +EAttachment * +e_attachment_new_for_path (const gchar *path) +{ + EAttachment *attachment; + GFile *file; + + g_return_val_if_fail (path != NULL, NULL); + + file = g_file_new_for_path (path); + attachment = g_object_new (E_TYPE_ATTACHMENT, "file", file, NULL); + g_object_unref (file); + + return attachment; +} + +EAttachment * +e_attachment_new_for_uri (const gchar *uri) +{ + EAttachment *attachment; + GFile *file; + + g_return_val_if_fail (uri != NULL, NULL); + + file = g_file_new_for_uri (uri); + attachment = g_object_new (E_TYPE_ATTACHMENT, "file", file, NULL); + g_object_unref (file); + + return attachment; +} + +EAttachment * +e_attachment_new_for_message (CamelMimeMessage *message) +{ + CamelDataWrapper *wrapper; + CamelMimePart *mime_part; + EAttachment *attachment; + GString *description; + const gchar *subject; + + g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), NULL); + + mime_part = camel_mime_part_new (); + camel_mime_part_set_disposition (mime_part, "inline"); + subject = camel_mime_message_get_subject (message); + + /* To Translators: This text is set as a description of an attached + * message when, for example, attaching it to a composer. When the + * message to be attached has also filled Subject, then this text is + * of form "Attached message - Subject", otherwise it's left as is. */ + description = g_string_new (_("Attached message")); + if (subject != NULL) + g_string_append_printf (description, " - %s", subject); + camel_mime_part_set_description (mime_part, description->str); + g_string_free (description, TRUE); + + wrapper = CAMEL_DATA_WRAPPER (message); + camel_medium_set_content (CAMEL_MEDIUM (mime_part), wrapper); + camel_mime_part_set_content_type (mime_part, "message/rfc822"); + + attachment = e_attachment_new (); + e_attachment_set_mime_part (attachment, mime_part); + g_object_unref (mime_part); + + return attachment; +} + +void +e_attachment_add_to_multipart (EAttachment *attachment, + CamelMultipart *multipart, + const gchar *default_charset) +{ + CamelContentType *content_type; + CamelDataWrapper *wrapper; + CamelMimePart *mime_part; + + /* XXX EMsgComposer might be a better place for this function. */ + + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + g_return_if_fail (CAMEL_IS_MULTIPART (multipart)); + + /* Still loading? Too bad. */ + mime_part = e_attachment_get_mime_part (attachment); + if (mime_part == NULL) + return; + + content_type = camel_mime_part_get_content_type (mime_part); + wrapper = camel_medium_get_content (CAMEL_MEDIUM (mime_part)); + + if (CAMEL_IS_MULTIPART (wrapper)) + goto exit; + + /* For text content, determine the best encoding and character set. */ + if (camel_content_type_is (content_type, "text", "*")) { + CamelTransferEncoding encoding; + CamelStream *filtered_stream; + CamelMimeFilter *filter; + CamelStream *stream; + const gchar *charset; + + charset = camel_content_type_param (content_type, "charset"); + + /* Determine the best encoding by writing the MIME + * part to a NULL stream with a "bestenc" filter. */ + stream = camel_stream_null_new (); + filtered_stream = camel_stream_filter_new (stream); + filter = camel_mime_filter_bestenc_new ( + CAMEL_BESTENC_GET_ENCODING); + camel_stream_filter_add ( + CAMEL_STREAM_FILTER (filtered_stream), + CAMEL_MIME_FILTER (filter)); + camel_data_wrapper_decode_to_stream_sync ( + wrapper, filtered_stream, NULL, NULL); + g_object_unref (filtered_stream); + g_object_unref (stream); + + /* Retrieve the best encoding from the filter. */ + encoding = camel_mime_filter_bestenc_get_best_encoding ( + CAMEL_MIME_FILTER_BESTENC (filter), + CAMEL_BESTENC_8BIT); + camel_mime_part_set_encoding (mime_part, encoding); + g_object_unref (filter); + + if (encoding == CAMEL_TRANSFER_ENCODING_7BIT) { + /* The text fits within us-ascii, so this is safe. + * FIXME Check that this isn't iso-2022-jp? */ + default_charset = "us-ascii"; + + } else if (charset == NULL && default_charset == NULL) { + default_charset = attachment_get_default_charset (); + /* FIXME Check that this fits within the + * default_charset and if not, find one + * that does and/or allow the user to + * specify. */ + } + + if (charset == NULL) { + gchar *type; + + camel_content_type_set_param ( + content_type, "charset", default_charset); + type = camel_content_type_format (content_type); + camel_mime_part_set_content_type (mime_part, type); + g_free (type); + } + + /* Otherwise, unless it's a message/rfc822, Base64 encode it. */ + } else if (!CAMEL_IS_MIME_MESSAGE (wrapper)) + camel_mime_part_set_encoding ( + mime_part, CAMEL_TRANSFER_ENCODING_BASE64); + +exit: + camel_multipart_add_part (multipart, mime_part); +} + +void +e_attachment_cancel (EAttachment *attachment) +{ + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + g_cancellable_cancel (attachment->priv->cancellable); +} + +gboolean +e_attachment_get_can_show (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + + return attachment->priv->can_show; +} + +void +e_attachment_set_can_show (EAttachment *attachment, + gboolean can_show) +{ + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + attachment->priv->can_show = can_show; + + g_object_notify (G_OBJECT (attachment), "can-show"); +} + +const gchar * +e_attachment_get_disposition (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + return attachment->priv->disposition; +} + +void +e_attachment_set_disposition (EAttachment *attachment, + const gchar *disposition) +{ + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + g_free (attachment->priv->disposition); + attachment->priv->disposition = g_strdup (disposition); + + g_object_notify (G_OBJECT (attachment), "disposition"); +} + +GFile * +e_attachment_get_file (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + return attachment->priv->file; +} + +void +e_attachment_set_file (EAttachment *attachment, + GFile *file) +{ + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + if (file != NULL) { + g_return_if_fail (G_IS_FILE (file)); + g_object_ref (file); + } + + if (attachment->priv->file != NULL) + g_object_unref (attachment->priv->file); + + attachment->priv->file = file; + + g_object_notify (G_OBJECT (attachment), "file"); +} + +GFileInfo * +e_attachment_get_file_info (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + return attachment->priv->file_info; +} + +void +e_attachment_set_file_info (EAttachment *attachment, + GFileInfo *file_info) +{ + GtkTreeRowReference *reference; + GIcon *icon; + + reference = e_attachment_get_reference (attachment); + + if (file_info != NULL) + g_object_ref (file_info); + + if (attachment->priv->file_info != NULL) + g_object_unref (attachment->priv->file_info); + + attachment->priv->file_info = file_info; + + /* If the GFileInfo contains a GThemedIcon, append a + * fallback icon name to ensure we display something. */ + icon = g_file_info_get_icon (file_info); + if (G_IS_THEMED_ICON (icon)) + g_themed_icon_append_name ( + G_THEMED_ICON (icon), DEFAULT_ICON_NAME); + + g_object_notify (G_OBJECT (attachment), "file-info"); + + /* Tell the EAttachmentStore its total size changed. */ + if (gtk_tree_row_reference_valid (reference)) { + GtkTreeModel *model; + model = gtk_tree_row_reference_get_model (reference); + g_object_notify (G_OBJECT (model), "total-size"); + } +} + +/** + * e_attachment_get_mime_type: + * + * Returns mime_type part of the file_info as a newly allocated string, + * which should be freed with g_free(). + * Returns NULL, if mime_type not found or set on the attachment. + **/ +gchar * +e_attachment_get_mime_type (EAttachment *attachment) +{ + GFileInfo *file_info; + const gchar *content_type; + gchar *mime_type; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + file_info = e_attachment_get_file_info (attachment); + if (file_info == NULL) + return NULL; + + content_type = g_file_info_get_content_type (file_info); + if (content_type == NULL) + return NULL; + + mime_type = g_content_type_get_mime_type (content_type); + if (!mime_type) + return NULL; + + camel_strdown (mime_type); + + return mime_type; +} + +GIcon * +e_attachment_get_icon (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + return attachment->priv->icon; +} + +gboolean +e_attachment_get_loading (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + + return attachment->priv->loading; +} + +CamelMimePart * +e_attachment_get_mime_part (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + return attachment->priv->mime_part; +} + +void +e_attachment_set_mime_part (EAttachment *attachment, + CamelMimePart *mime_part) +{ + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + if (mime_part != NULL) { + g_return_if_fail (CAMEL_IS_MIME_PART (mime_part)); + g_object_ref (mime_part); + } + + if (attachment->priv->mime_part != NULL) + g_object_unref (attachment->priv->mime_part); + + attachment->priv->mime_part = mime_part; + + g_object_notify (G_OBJECT (attachment), "mime-part"); +} + +gint +e_attachment_get_percent (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), 0); + + return attachment->priv->percent; +} + +GtkTreeRowReference * +e_attachment_get_reference (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + return attachment->priv->reference; +} + +void +e_attachment_set_reference (EAttachment *attachment, + GtkTreeRowReference *reference) +{ + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + if (reference != NULL) + reference = gtk_tree_row_reference_copy (reference); + + gtk_tree_row_reference_free (attachment->priv->reference); + attachment->priv->reference = reference; + + g_object_notify (G_OBJECT (attachment), "reference"); +} + +gboolean +e_attachment_get_saving (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + + return attachment->priv->saving; +} + +gboolean +e_attachment_get_shown (EAttachment *attachment) +{ + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + + return attachment->priv->shown; +} + +void +e_attachment_set_shown (EAttachment *attachment, + gboolean shown) +{ + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + attachment->priv->shown = shown; + + g_object_notify (G_OBJECT (attachment), "shown"); +} + +camel_cipher_validity_encrypt_t +e_attachment_get_encrypted (EAttachment *attachment) +{ + g_return_val_if_fail ( + E_IS_ATTACHMENT (attachment), + CAMEL_CIPHER_VALIDITY_ENCRYPT_NONE); + + return attachment->priv->encrypted; +} + +void +e_attachment_set_encrypted (EAttachment *attachment, + camel_cipher_validity_encrypt_t encrypted) +{ + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + attachment->priv->encrypted = encrypted; + + g_object_notify (G_OBJECT (attachment), "encrypted"); +} + +camel_cipher_validity_sign_t +e_attachment_get_signed (EAttachment *attachment) +{ + g_return_val_if_fail ( + E_IS_ATTACHMENT (attachment), + CAMEL_CIPHER_VALIDITY_SIGN_NONE); + + return attachment->priv->signed_; +} + +void +e_attachment_set_signed (EAttachment *attachment, + camel_cipher_validity_sign_t signed_) +{ + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + attachment->priv->signed_ = signed_; + + g_object_notify (G_OBJECT (attachment), "signed"); +} + +const gchar * +e_attachment_get_description (EAttachment *attachment) +{ + GFileInfo *file_info; + const gchar *attribute; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + attribute = G_FILE_ATTRIBUTE_STANDARD_DESCRIPTION; + file_info = e_attachment_get_file_info (attachment); + + if (file_info == NULL) + return NULL; + + return g_file_info_get_attribute_string (file_info, attribute); +} + +const gchar * +e_attachment_get_thumbnail_path (EAttachment *attachment) +{ + GFileInfo *file_info; + const gchar *attribute; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + attribute = G_FILE_ATTRIBUTE_THUMBNAIL_PATH; + file_info = e_attachment_get_file_info (attachment); + + if (file_info == NULL) + return NULL; + + return g_file_info_get_attribute_byte_string (file_info, attribute); +} + +gboolean +e_attachment_is_rfc822 (EAttachment *attachment) +{ + gchar *mime_type; + gboolean is_rfc822; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + + mime_type = e_attachment_get_mime_type (attachment); + is_rfc822 = mime_type && g_ascii_strcasecmp (mime_type, "message/rfc822") == 0; + g_free (mime_type); + + return is_rfc822; +} + +GList * +e_attachment_list_apps (EAttachment *attachment) +{ + GList *app_info_list; + GList *guessed_infos; + GFileInfo *file_info; + const gchar *content_type; + const gchar *display_name; + gboolean type_is_unknown; + gchar *allocated; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + + file_info = e_attachment_get_file_info (attachment); + if (file_info == NULL) + return NULL; + + content_type = g_file_info_get_content_type (file_info); + display_name = g_file_info_get_display_name (file_info); + g_return_val_if_fail (content_type != NULL, NULL); + + app_info_list = g_app_info_get_all_for_type (content_type); + type_is_unknown = g_content_type_is_unknown (content_type); + + if (app_info_list != NULL && !type_is_unknown) + goto exit; + + if (display_name == NULL) + goto exit; + + allocated = g_content_type_guess (display_name, NULL, 0, NULL); + guessed_infos = g_app_info_get_all_for_type (allocated); + app_info_list = g_list_concat (guessed_infos, app_info_list); + g_free (allocated); + +exit: + return app_info_list; +} + +/************************* e_attachment_load_async() *************************/ + +typedef struct _LoadContext LoadContext; + +struct _LoadContext { + EAttachment *attachment; + CamelMimePart *mime_part; + GSimpleAsyncResult *simple; + + GInputStream *input_stream; + GOutputStream *output_stream; + GFileInfo *file_info; + goffset total_num_bytes; + gssize bytes_read; + gchar buffer[4096]; +}; + +/* Forward Declaration */ +static void +attachment_load_stream_read_cb (GInputStream *input_stream, + GAsyncResult *result, + LoadContext *load_context); + +static LoadContext * +attachment_load_context_new (EAttachment *attachment, + GAsyncReadyCallback callback, + gpointer user_data) +{ + LoadContext *load_context; + GSimpleAsyncResult *simple; + + simple = g_simple_async_result_new ( + G_OBJECT (attachment), callback, + user_data, e_attachment_load_async); + + load_context = g_slice_new0 (LoadContext); + load_context->attachment = g_object_ref (attachment); + load_context->simple = simple; + + attachment_set_loading (load_context->attachment, TRUE); + + return load_context; +} + +static void +attachment_load_context_free (LoadContext *load_context) +{ + g_object_unref (load_context->attachment); + + if (load_context->mime_part != NULL) + g_object_unref (load_context->mime_part); + + if (load_context->simple) + g_object_unref (load_context->simple); + + if (load_context->input_stream != NULL) + g_object_unref (load_context->input_stream); + + if (load_context->output_stream != NULL) + g_object_unref (load_context->output_stream); + + if (load_context->file_info != NULL) + g_object_unref (load_context->file_info); + + g_slice_free (LoadContext, load_context); +} + +static gboolean +attachment_load_check_for_error (LoadContext *load_context, + GError *error) +{ + GSimpleAsyncResult *simple; + + if (error == NULL) + return FALSE; + + simple = load_context->simple; + g_simple_async_result_take_error (simple, error); + g_simple_async_result_complete (simple); + + attachment_load_context_free (load_context); + + return TRUE; +} + +static void +attachment_load_finish (LoadContext *load_context) +{ + GFileInfo *file_info; + EAttachment *attachment; + GMemoryOutputStream *output_stream; + GSimpleAsyncResult *simple; + CamelDataWrapper *wrapper; + CamelMimePart *mime_part; + CamelStream *stream; + const gchar *attribute; + const gchar *content_type; + const gchar *display_name; + const gchar *description; + const gchar *disposition; + gchar *mime_type; + gpointer data; + gsize size; + + simple = load_context->simple; + + file_info = load_context->file_info; + attachment = load_context->attachment; + output_stream = G_MEMORY_OUTPUT_STREAM (load_context->output_stream); + + if (e_attachment_is_rfc822 (attachment)) + wrapper = (CamelDataWrapper *) camel_mime_message_new (); + else + wrapper = camel_data_wrapper_new (); + + content_type = g_file_info_get_content_type (file_info); + mime_type = g_content_type_get_mime_type (content_type); + + data = g_memory_output_stream_get_data (output_stream); + size = g_memory_output_stream_get_data_size (output_stream); + + stream = camel_stream_mem_new_with_buffer (data, size); + camel_data_wrapper_construct_from_stream_sync ( + wrapper, stream, NULL, NULL); + camel_data_wrapper_set_mime_type (wrapper, mime_type); + camel_stream_close (stream, NULL, NULL); + g_object_unref (stream); + + mime_part = camel_mime_part_new (); + camel_medium_set_content (CAMEL_MEDIUM (mime_part), wrapper); + + g_object_unref (wrapper); + g_free (mime_type); + + display_name = g_file_info_get_display_name (file_info); + if (display_name != NULL) + camel_mime_part_set_filename (mime_part, display_name); + + attribute = G_FILE_ATTRIBUTE_STANDARD_DESCRIPTION; + description = g_file_info_get_attribute_string (file_info, attribute); + if (description != NULL) + camel_mime_part_set_description (mime_part, description); + + disposition = e_attachment_get_disposition (attachment); + if (disposition != NULL) + camel_mime_part_set_disposition (mime_part, disposition); + + /* Correctly report the size of zero length special files. */ + if (g_file_info_get_size (file_info) == 0) + g_file_info_set_size (file_info, size); + + load_context->mime_part = mime_part; + + g_simple_async_result_set_op_res_gpointer ( + simple, load_context, (GDestroyNotify) attachment_load_context_free); + + g_simple_async_result_complete (simple); + + /* make sure it's freed on operation end */ + load_context->simple = NULL; + g_object_unref (simple); +} + +static void +attachment_load_write_cb (GOutputStream *output_stream, + GAsyncResult *result, + LoadContext *load_context) +{ + EAttachment *attachment; + GCancellable *cancellable; + GInputStream *input_stream; + gssize bytes_written; + GError *error = NULL; + + bytes_written = g_output_stream_write_finish ( + output_stream, result, &error); + + if (attachment_load_check_for_error (load_context, error)) + return; + + attachment = load_context->attachment; + cancellable = attachment->priv->cancellable; + input_stream = load_context->input_stream; + + attachment_progress_cb ( + g_seekable_tell (G_SEEKABLE (output_stream)), + load_context->total_num_bytes, attachment); + + if (bytes_written < load_context->bytes_read) { + g_memmove ( + load_context->buffer, + load_context->buffer + bytes_written, + load_context->bytes_read - bytes_written); + load_context->bytes_read -= bytes_written; + + g_output_stream_write_async ( + output_stream, + load_context->buffer, + load_context->bytes_read, + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_load_write_cb, + load_context); + } else + g_input_stream_read_async ( + input_stream, + load_context->buffer, + sizeof (load_context->buffer), + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_load_stream_read_cb, + load_context); +} + +static void +attachment_load_stream_read_cb (GInputStream *input_stream, + GAsyncResult *result, + LoadContext *load_context) +{ + EAttachment *attachment; + GCancellable *cancellable; + GOutputStream *output_stream; + gssize bytes_read; + GError *error = NULL; + + bytes_read = g_input_stream_read_finish ( + input_stream, result, &error); + + if (attachment_load_check_for_error (load_context, error)) + return; + + if (bytes_read == 0) { + attachment_load_finish (load_context); + return; + } + + attachment = load_context->attachment; + cancellable = attachment->priv->cancellable; + output_stream = load_context->output_stream; + load_context->bytes_read = bytes_read; + + g_output_stream_write_async ( + output_stream, + load_context->buffer, + load_context->bytes_read, + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_load_write_cb, + load_context); +} + +static void +attachment_load_file_read_cb (GFile *file, + GAsyncResult *result, + LoadContext *load_context) +{ + EAttachment *attachment; + GCancellable *cancellable; + GFileInputStream *input_stream; + GOutputStream *output_stream; + GError *error = NULL; + + /* Input stream might be NULL, so don't use cast macro. */ + input_stream = g_file_read_finish (file, result, &error); + load_context->input_stream = (GInputStream *) input_stream; + + if (attachment_load_check_for_error (load_context, error)) + return; + + /* Load the contents into a GMemoryOutputStream. */ + output_stream = g_memory_output_stream_new ( + NULL, 0, g_realloc, g_free); + + attachment = load_context->attachment; + cancellable = attachment->priv->cancellable; + load_context->output_stream = output_stream; + + g_input_stream_read_async ( + load_context->input_stream, + load_context->buffer, + sizeof (load_context->buffer), + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_load_stream_read_cb, + load_context); +} + +static void +attachment_load_query_info_cb (GFile *file, + GAsyncResult *result, + LoadContext *load_context) +{ + EAttachment *attachment; + GCancellable *cancellable; + GFileInfo *file_info; + GError *error = NULL; + + attachment = load_context->attachment; + cancellable = attachment->priv->cancellable; + + file_info = g_file_query_info_finish (file, result, &error); + if (attachment_load_check_for_error (load_context, error)) + return; + + e_attachment_set_file_info (attachment, file_info); + load_context->file_info = file_info; + + load_context->total_num_bytes = g_file_info_get_size (file_info); + + g_file_read_async ( + file, G_PRIORITY_DEFAULT, + cancellable, (GAsyncReadyCallback) + attachment_load_file_read_cb, load_context); +} + +#define ATTACHMENT_LOAD_CONTEXT "attachment-load-context-data" + +static void +attachment_load_from_mime_part_thread (GSimpleAsyncResult *simple, + GObject *object, + GCancellable *cancellable) +{ + LoadContext *load_context; + GFileInfo *file_info; + EAttachment *attachment; + CamelContentType *content_type; + CamelMimePart *mime_part; + const gchar *attribute; + const gchar *string; + gchar *allocated; + CamelStream *null; + CamelDataWrapper *dw; + + load_context = g_object_get_data (G_OBJECT (simple), ATTACHMENT_LOAD_CONTEXT); + g_return_if_fail (load_context != NULL); + g_object_set_data (G_OBJECT (simple), ATTACHMENT_LOAD_CONTEXT, NULL); + + attachment = load_context->attachment; + mime_part = e_attachment_get_mime_part (attachment); + + file_info = g_file_info_new (); + load_context->file_info = file_info; + + content_type = camel_mime_part_get_content_type (mime_part); + allocated = camel_content_type_simple (content_type); + if (allocated != NULL) { + GIcon *icon; + gchar *cp; + + /* GIO expects lowercase MIME types. */ + for (cp = allocated; *cp != '\0'; cp++) + *cp = g_ascii_tolower (*cp); + + /* Swap the MIME type for a content type. */ + cp = g_content_type_from_mime_type (allocated); + g_free (allocated); + allocated = cp; + + /* Use the MIME part's filename if we have to. */ + if (g_content_type_is_unknown (allocated)) { + string = camel_mime_part_get_filename (mime_part); + if (string != NULL) { + g_free (allocated); + allocated = g_content_type_guess ( + string, NULL, 0, NULL); + } + } + + g_file_info_set_content_type (file_info, allocated); + + icon = g_content_type_get_icon (allocated); + if (icon != NULL) { + g_file_info_set_icon (file_info, icon); + g_object_unref (icon); + } + } + g_free (allocated); + + /* Strip any path components from the filename. */ + string = camel_mime_part_get_filename (mime_part); + if (string == NULL) + /* Translators: Default attachment filename. */ + string = _("attachment.dat"); + allocated = g_path_get_basename (string); + g_file_info_set_display_name (file_info, allocated); + g_free (allocated); + + attribute = G_FILE_ATTRIBUTE_STANDARD_DESCRIPTION; + string = camel_mime_part_get_description (mime_part); + if (string != NULL) + g_file_info_set_attribute_string ( + file_info, attribute, string); + + dw = camel_medium_get_content (CAMEL_MEDIUM (mime_part)); + null = camel_stream_null_new (); + /* this actually downloads the part and makes it available later */ + camel_data_wrapper_decode_to_stream_sync (dw, null, attachment->priv->cancellable, NULL); + g_file_info_set_size (file_info, CAMEL_STREAM_NULL (null)->written); + g_object_unref (null); + + load_context->mime_part = g_object_ref (mime_part); + + /* make sure it's freed on operation end */ + g_object_unref (load_context->simple); + load_context->simple = NULL; + + g_simple_async_result_set_op_res_gpointer ( + simple, load_context, + (GDestroyNotify) attachment_load_context_free); +} + +void +e_attachment_load_async (EAttachment *attachment, + GAsyncReadyCallback callback, + gpointer user_data) +{ + LoadContext *load_context; + GCancellable *cancellable; + CamelMimePart *mime_part; + GFile *file; + + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + if (e_attachment_get_loading (attachment)) { + g_simple_async_report_error_in_idle ( + G_OBJECT (attachment), callback, user_data, + G_IO_ERROR, G_IO_ERROR_BUSY, + _("A load operation is already in progress")); + return; + } + + if (e_attachment_get_saving (attachment)) { + g_simple_async_report_error_in_idle ( + G_OBJECT (attachment), callback, user_data, + G_IO_ERROR, G_IO_ERROR_BUSY, + _("A save operation is already in progress")); + return; + } + + file = e_attachment_get_file (attachment); + mime_part = e_attachment_get_mime_part (attachment); + g_return_if_fail (file != NULL || mime_part != NULL); + + load_context = attachment_load_context_new ( + attachment, callback, user_data); + + cancellable = attachment->priv->cancellable; + g_cancellable_reset (cancellable); + + if (file != NULL) { + g_file_query_info_async ( + file, ATTACHMENT_QUERY, + G_FILE_QUERY_INFO_NONE,G_PRIORITY_DEFAULT, + cancellable, (GAsyncReadyCallback) + attachment_load_query_info_cb, load_context); + + } else if (mime_part != NULL) { + g_object_set_data (G_OBJECT (load_context->simple), ATTACHMENT_LOAD_CONTEXT, load_context); + + g_simple_async_result_run_in_thread ( + load_context->simple, + attachment_load_from_mime_part_thread, + G_PRIORITY_DEFAULT, + cancellable); + } +} + +gboolean +e_attachment_load_finish (EAttachment *attachment, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + const LoadContext *load_context; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + g_return_val_if_fail (G_IS_SIMPLE_ASYNC_RESULT (result), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + load_context = g_simple_async_result_get_op_res_gpointer (simple); + if (load_context && load_context->mime_part) { + const gchar *string; + + string = camel_mime_part_get_disposition (load_context->mime_part); + e_attachment_set_disposition (attachment, string); + + e_attachment_set_file_info (attachment, load_context->file_info); + e_attachment_set_mime_part (attachment, load_context->mime_part); + } + + g_simple_async_result_propagate_error (simple, error); + + attachment_set_loading (attachment, FALSE); + + return (load_context != NULL); +} + +void +e_attachment_load_handle_error (EAttachment *attachment, + GAsyncResult *result, + GtkWindow *parent) +{ + GtkWidget *dialog; + GFileInfo *file_info; + GtkTreeRowReference *reference; + const gchar *display_name; + const gchar *primary_text; + GError *error = NULL; + + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + g_return_if_fail (G_IS_ASYNC_RESULT (result)); + g_return_if_fail (!parent || GTK_IS_WINDOW (parent)); + + if (e_attachment_load_finish (attachment, result, &error)) + return; + + /* XXX Calling EAttachmentStore functions from here violates + * the abstraction, but for now it's not hurting anything. */ + reference = e_attachment_get_reference (attachment); + if (gtk_tree_row_reference_valid (reference)) { + GtkTreeModel *model; + + model = gtk_tree_row_reference_get_model (reference); + + e_attachment_store_remove_attachment ( + E_ATTACHMENT_STORE (model), attachment); + } + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_error_free (error); + return; + } + + file_info = e_attachment_get_file_info (attachment); + + if (file_info != NULL) + display_name = g_file_info_get_display_name (file_info); + else + display_name = NULL; + + if (display_name != NULL) + primary_text = g_strdup_printf ( + _("Could not load '%s'"), display_name); + else + primary_text = g_strdup_printf ( + _("Could not load the attachment")); + + dialog = gtk_message_dialog_new_with_markup ( + parent, GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "<big><b>%s</b></big>", primary_text); + + gtk_message_dialog_format_secondary_text ( + GTK_MESSAGE_DIALOG (dialog), "%s", error->message); + + gtk_dialog_run (GTK_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + g_error_free (error); +} + +gboolean +e_attachment_load (EAttachment *attachment, + GError **error) +{ + EAsyncClosure *closure; + GAsyncResult *result; + gboolean success; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + + closure = e_async_closure_new (); + + e_attachment_load_async (attachment, e_async_closure_callback, closure); + + result = e_async_closure_wait (closure); + + success = e_attachment_load_finish (attachment, result, error); + + e_async_closure_free (closure); + + return success; +} + +/************************* e_attachment_open_async() *************************/ + +typedef struct _OpenContext OpenContext; + +struct _OpenContext { + EAttachment *attachment; + GSimpleAsyncResult *simple; + + GAppInfo *app_info; +}; + +static OpenContext * +attachment_open_context_new (EAttachment *attachment, + GAsyncReadyCallback callback, + gpointer user_data) +{ + OpenContext *open_context; + GSimpleAsyncResult *simple; + + simple = g_simple_async_result_new ( + G_OBJECT (attachment), callback, + user_data, e_attachment_open_async); + + open_context = g_slice_new0 (OpenContext); + open_context->attachment = g_object_ref (attachment); + open_context->simple = simple; + + return open_context; +} + +static void +attachment_open_context_free (OpenContext *open_context) +{ + g_object_unref (open_context->attachment); + g_object_unref (open_context->simple); + + if (open_context->app_info != NULL) + g_object_unref (open_context->app_info); + + g_slice_free (OpenContext, open_context); +} + +static gboolean +attachment_open_check_for_error (OpenContext *open_context, + GError *error) +{ + GSimpleAsyncResult *simple; + + if (error == NULL) + return FALSE; + + simple = open_context->simple; + g_simple_async_result_take_error (simple, error); + g_simple_async_result_complete (simple); + + attachment_open_context_free (open_context); + + return TRUE; +} + +static void +attachment_open_file (GFile *file, + OpenContext *open_context) +{ + GdkAppLaunchContext *context; + GSimpleAsyncResult *simple; + GdkDisplay *display; + gboolean success; + GError *error = NULL; + + simple = open_context->simple; + + display = gdk_display_get_default (); + context = gdk_display_get_app_launch_context (display); + + if (open_context->app_info != NULL) { + GList *file_list; + + file_list = g_list_prepend (NULL, file); + success = g_app_info_launch ( + open_context->app_info, file_list, + G_APP_LAUNCH_CONTEXT (context), &error); + g_list_free (file_list); + } else { + gchar *uri; + + uri = g_file_get_uri (file); + success = g_app_info_launch_default_for_uri ( + uri, G_APP_LAUNCH_CONTEXT (context), &error); + g_free (uri); + } + + g_object_unref (context); + + g_simple_async_result_set_op_res_gboolean (simple, success); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); + + g_simple_async_result_complete (simple); + attachment_open_context_free (open_context); +} + +static void +attachment_open_save_finished_cb (EAttachment *attachment, + GAsyncResult *result, + OpenContext *open_context) +{ + GFile *file; + gchar *path; + GError *error = NULL; + + file = e_attachment_save_finish (attachment, result, &error); + + if (attachment_open_check_for_error (open_context, error)) + return; + + /* Make the temporary file read-only. + * + * This step is non-critical, so if an error occurs just + * emit a warning and move on. + * + * XXX I haven't figured out how to do this through GIO. + * Attempting to set the "access::can-write" attribute via + * g_file_set_attribute() returned G_IO_ERROR_NOT_SUPPORTED + * and the only other possibility I see is "unix::mode", + * which is obviously not portable. + */ + path = g_file_get_path (file); +#ifndef G_OS_WIN32 + if (g_chmod (path, S_IRUSR | S_IRGRP | S_IROTH) < 0) + g_warning ("%s", g_strerror (errno)); +#endif + g_free (path); + + attachment_open_file (file, open_context); + g_object_unref (file); +} + +static void +attachment_open_save_temporary (OpenContext *open_context) +{ + GFile *temp_directory; + gchar *template; + gchar *path; + GError *error = NULL; + + errno = 0; + + /* Save the file to a temporary directory. + * We use a directory so the files can retain their basenames. + * XXX This could trigger a blocking temp directory cleanup. */ + template = g_strdup_printf (PACKAGE "-%s-XXXXXX", g_get_user_name ()); + path = e_mkdtemp (template); + g_free (template); + + /* XXX Let's hope errno got set properly. */ + if (path == NULL) + g_set_error ( + &error, G_FILE_ERROR, + g_file_error_from_errno (errno), + "%s", g_strerror (errno)); + + /* We already know if there's an error, but this does the cleanup. */ + if (attachment_open_check_for_error (open_context, error)) + return; + + temp_directory = g_file_new_for_path (path); + + e_attachment_save_async ( + open_context->attachment, + temp_directory, (GAsyncReadyCallback) + attachment_open_save_finished_cb, open_context); + + g_object_unref (temp_directory); + g_free (path); +} + +void +e_attachment_open_async (EAttachment *attachment, + GAppInfo *app_info, + GAsyncReadyCallback callback, + gpointer user_data) +{ + OpenContext *open_context; + CamelMimePart *mime_part; + GFile *file; + + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + + file = e_attachment_get_file (attachment); + mime_part = e_attachment_get_mime_part (attachment); + g_return_if_fail (file != NULL || mime_part != NULL); + + open_context = attachment_open_context_new ( + attachment, callback, user_data); + + if (G_IS_APP_INFO (app_info)) + open_context->app_info = g_object_ref (app_info); + + /* If the attachment already references a GFile, we can launch + * the application directly. Otherwise we have to save the MIME + * part to a temporary file and launch the application from that. */ + if (file != NULL) { + attachment_open_file (file, open_context); + + } else if (mime_part != NULL) + attachment_open_save_temporary (open_context); +} + +gboolean +e_attachment_open_finish (EAttachment *attachment, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + gboolean success; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + g_return_val_if_fail (G_IS_SIMPLE_ASYNC_RESULT (result), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + success = g_simple_async_result_get_op_res_gboolean (simple); + g_simple_async_result_propagate_error (simple, error); + + return success; +} + +void +e_attachment_open_handle_error (EAttachment *attachment, + GAsyncResult *result, + GtkWindow *parent) +{ + GtkWidget *dialog; + GFileInfo *file_info; + const gchar *display_name; + const gchar *primary_text; + GError *error = NULL; + + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + g_return_if_fail (G_IS_ASYNC_RESULT (result)); + g_return_if_fail (GTK_IS_WINDOW (parent)); + + if (e_attachment_open_finish (attachment, result, &error)) + return; + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + file_info = e_attachment_get_file_info (attachment); + + if (file_info != NULL) + display_name = g_file_info_get_display_name (file_info); + else + display_name = NULL; + + if (display_name != NULL) + primary_text = g_strdup_printf ( + _("Could not open '%s'"), display_name); + else + primary_text = g_strdup_printf ( + _("Could not open the attachment")); + + dialog = gtk_message_dialog_new_with_markup ( + parent, GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "<big><b>%s</b></big>", primary_text); + + gtk_message_dialog_format_secondary_text ( + GTK_MESSAGE_DIALOG (dialog), "%s", error->message); + + gtk_dialog_run (GTK_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + g_error_free (error); +} + +gboolean +e_attachment_open (EAttachment *attachment, + GAppInfo *app_info, + GError **error) +{ + EAsyncClosure *closure; + GAsyncResult *result; + gboolean success; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + + closure = e_async_closure_new (); + + e_attachment_open_async (attachment, app_info, e_async_closure_callback, closure); + + result = e_async_closure_wait (closure); + + success = e_attachment_open_finish (attachment, result, error); + + e_async_closure_free (closure); + + return success; +} + +/************************* e_attachment_save_async() *************************/ + +typedef struct _SaveContext SaveContext; + +struct _SaveContext { + EAttachment *attachment; + GSimpleAsyncResult *simple; + + GFile *directory; + GFile *destination; + GInputStream *input_stream; + GOutputStream *output_stream; + goffset total_num_bytes; + gssize bytes_read; + gchar buffer[4096]; + gint count; +}; + +/* Forward Declaration */ +static void +attachment_save_read_cb (GInputStream *input_stream, + GAsyncResult *result, + SaveContext *save_context); + +static SaveContext * +attachment_save_context_new (EAttachment *attachment, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SaveContext *save_context; + GSimpleAsyncResult *simple; + + simple = g_simple_async_result_new ( + G_OBJECT (attachment), callback, + user_data, e_attachment_save_async); + + save_context = g_slice_new0 (SaveContext); + save_context->attachment = g_object_ref (attachment); + save_context->simple = simple; + + attachment_set_saving (save_context->attachment, TRUE); + + return save_context; +} + +static void +attachment_save_context_free (SaveContext *save_context) +{ + g_object_unref (save_context->attachment); + g_object_unref (save_context->simple); + + if (save_context->directory != NULL) + g_object_unref (save_context->directory); + + if (save_context->destination != NULL) + g_object_unref (save_context->destination); + + if (save_context->input_stream != NULL) + g_object_unref (save_context->input_stream); + + if (save_context->output_stream != NULL) + g_object_unref (save_context->output_stream); + + g_slice_free (SaveContext, save_context); +} + +static gboolean +attachment_save_check_for_error (SaveContext *save_context, + GError *error) +{ + GSimpleAsyncResult *simple; + + if (error == NULL) + return FALSE; + + simple = save_context->simple; + g_simple_async_result_take_error (simple, error); + g_simple_async_result_complete (simple); + + attachment_save_context_free (save_context); + + return TRUE; +} + +static GFile * +attachment_save_new_candidate (SaveContext *save_context) +{ + GFile *candidate; + GFileInfo *file_info; + EAttachment *attachment; + const gchar *display_name = NULL; + gchar *basename; + + attachment = save_context->attachment; + file_info = e_attachment_get_file_info (attachment); + + if (file_info != NULL) + display_name = g_file_info_get_display_name (file_info); + if (display_name == NULL) + /* Translators: Default attachment filename. */ + display_name = _("attachment.dat"); + + if (save_context->count == 0) + basename = g_strdup (display_name); + else { + GString *string; + const gchar *ext; + gsize length; + + string = g_string_sized_new (strlen (display_name)); + ext = g_utf8_strchr (display_name, -1, '.'); + + if (ext != NULL) + length = ext - display_name; + else + length = strlen (display_name); + + g_string_append_len (string, display_name, length); + g_string_append_printf (string, " (%d)", save_context->count); + g_string_append (string, (ext != NULL) ? ext : ""); + + basename = g_string_free (string, FALSE); + } + + save_context->count++; + + candidate = g_file_get_child (save_context->directory, basename); + + g_free (basename); + + return candidate; +} + +static void +attachment_save_write_cb (GOutputStream *output_stream, + GAsyncResult *result, + SaveContext *save_context) +{ + EAttachment *attachment; + GCancellable *cancellable; + GInputStream *input_stream; + gssize bytes_written; + GError *error = NULL; + + bytes_written = g_output_stream_write_finish ( + output_stream, result, &error); + + if (attachment_save_check_for_error (save_context, error)) + return; + + attachment = save_context->attachment; + cancellable = attachment->priv->cancellable; + input_stream = save_context->input_stream; + + if (bytes_written < save_context->bytes_read) { + g_memmove ( + save_context->buffer, + save_context->buffer + bytes_written, + save_context->bytes_read - bytes_written); + save_context->bytes_read -= bytes_written; + + g_output_stream_write_async ( + output_stream, + save_context->buffer, + save_context->bytes_read, + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_save_write_cb, + save_context); + } else + g_input_stream_read_async ( + input_stream, + save_context->buffer, + sizeof (save_context->buffer), + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_save_read_cb, + save_context); +} + +static void +attachment_save_read_cb (GInputStream *input_stream, + GAsyncResult *result, + SaveContext *save_context) +{ + EAttachment *attachment; + GCancellable *cancellable; + GOutputStream *output_stream; + gssize bytes_read; + GError *error = NULL; + + bytes_read = g_input_stream_read_finish ( + input_stream, result, &error); + + if (attachment_save_check_for_error (save_context, error)) + return; + + if (bytes_read == 0) { + GSimpleAsyncResult *simple; + GFile *destination; + + /* Steal the destination. */ + destination = save_context->destination; + save_context->destination = NULL; + + simple = save_context->simple; + g_simple_async_result_set_op_res_gpointer ( + simple, destination, (GDestroyNotify) g_object_unref); + g_simple_async_result_complete (simple); + + attachment_save_context_free (save_context); + + return; + } + + attachment = save_context->attachment; + cancellable = attachment->priv->cancellable; + output_stream = save_context->output_stream; + save_context->bytes_read = bytes_read; + + attachment_progress_cb ( + g_seekable_tell (G_SEEKABLE (input_stream)), + save_context->total_num_bytes, attachment); + + g_output_stream_write_async ( + output_stream, + save_context->buffer, + save_context->bytes_read, + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_save_write_cb, + save_context); +} + +static void +attachment_save_got_output_stream (SaveContext *save_context) +{ + GCancellable *cancellable; + GInputStream *input_stream; + CamelDataWrapper *wrapper; + CamelMimePart *mime_part; + CamelStream *stream; + EAttachment *attachment; + GByteArray *buffer; + + attachment = save_context->attachment; + cancellable = attachment->priv->cancellable; + mime_part = e_attachment_get_mime_part (attachment); + + /* Decode the MIME part to an in-memory buffer. We have to do + * this because CamelStream is synchronous-only, and using threads + * is dangerous because CamelDataWrapper is not reentrant. */ + buffer = g_byte_array_new (); + stream = camel_stream_mem_new (); + camel_stream_mem_set_byte_array (CAMEL_STREAM_MEM (stream), buffer); + wrapper = camel_medium_get_content (CAMEL_MEDIUM (mime_part)); + camel_data_wrapper_decode_to_stream_sync (wrapper, stream, NULL, NULL); + g_object_unref (stream); + + /* Load the buffer into a GMemoryInputStream. + * But watch out for zero length MIME parts. */ + input_stream = g_memory_input_stream_new (); + if (buffer->len > 0) + g_memory_input_stream_add_data ( + G_MEMORY_INPUT_STREAM (input_stream), + buffer->data, (gssize) buffer->len, + (GDestroyNotify) g_free); + save_context->input_stream = input_stream; + save_context->total_num_bytes = (goffset) buffer->len; + g_byte_array_free (buffer, FALSE); + + g_input_stream_read_async ( + input_stream, + save_context->buffer, + sizeof (save_context->buffer), + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_save_read_cb, + save_context); +} + +static void +attachment_save_create_cb (GFile *destination, + GAsyncResult *result, + SaveContext *save_context) +{ + EAttachment *attachment; + GCancellable *cancellable; + GFileOutputStream *output_stream; + GError *error = NULL; + + /* Output stream might be NULL, so don't use cast macro. */ + output_stream = g_file_create_finish (destination, result, &error); + save_context->output_stream = (GOutputStream *) output_stream; + + attachment = save_context->attachment; + cancellable = attachment->priv->cancellable; + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) { + destination = attachment_save_new_candidate (save_context); + + g_file_create_async ( + destination, G_FILE_CREATE_NONE, + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_save_create_cb, + save_context); + + g_object_unref (destination); + g_error_free (error); + return; + } + + if (attachment_save_check_for_error (save_context, error)) + return; + + save_context->destination = g_object_ref (destination); + attachment_save_got_output_stream (save_context); +} + +static void +attachment_save_replace_cb (GFile *destination, + GAsyncResult *result, + SaveContext *save_context) +{ + GFileOutputStream *output_stream; + GError *error = NULL; + + /* Output stream might be NULL, so don't use cast macro. */ + output_stream = g_file_replace_finish (destination, result, &error); + save_context->output_stream = (GOutputStream *) output_stream; + + if (attachment_save_check_for_error (save_context, error)) + return; + + save_context->destination = g_object_ref (destination); + attachment_save_got_output_stream (save_context); +} + +static void +attachment_save_query_info_cb (GFile *destination, + GAsyncResult *result, + SaveContext *save_context) +{ + EAttachment *attachment; + GCancellable *cancellable; + GFileInfo *file_info; + GFileType file_type; + GError *error = NULL; + + attachment = save_context->attachment; + cancellable = attachment->priv->cancellable; + + file_info = g_file_query_info_finish (destination, result, &error); + + /* G_IO_ERROR_NOT_FOUND just means we're creating a new file. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + g_error_free (error); + goto replace; + } + + if (attachment_save_check_for_error (save_context, error)) + return; + + file_type = g_file_info_get_file_type (file_info); + g_object_unref (file_info); + + if (file_type == G_FILE_TYPE_DIRECTORY) { + save_context->directory = g_object_ref (destination); + destination = attachment_save_new_candidate (save_context); + + g_file_create_async ( + destination, G_FILE_CREATE_NONE, + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_save_create_cb, + save_context); + + g_object_unref (destination); + + return; + } + +replace: + g_file_replace_async ( + destination, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, + G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) attachment_save_replace_cb, + save_context); +} + +void +e_attachment_save_async (EAttachment *attachment, + GFile *destination, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SaveContext *save_context; + GCancellable *cancellable; + + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + g_return_if_fail (G_IS_FILE (destination)); + + if (e_attachment_get_loading (attachment)) { + g_simple_async_report_error_in_idle ( + G_OBJECT (attachment), callback, user_data, + G_IO_ERROR, G_IO_ERROR_BUSY, + _("A load operation is already in progress")); + return; + } + + if (e_attachment_get_saving (attachment)) { + g_simple_async_report_error_in_idle ( + G_OBJECT (attachment), callback, user_data, + G_IO_ERROR, G_IO_ERROR_BUSY, + _("A save operation is already in progress")); + return; + } + + if (e_attachment_get_mime_part (attachment) == NULL) { + g_simple_async_report_error_in_idle ( + G_OBJECT (attachment), callback, user_data, + G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("Attachment contents not loaded")); + return; + } + + save_context = attachment_save_context_new ( + attachment, callback, user_data); + + cancellable = attachment->priv->cancellable; + g_cancellable_reset (cancellable); + + /* First we need to know if destination is a directory. */ + g_file_query_info_async ( + destination, G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, + cancellable, (GAsyncReadyCallback) + attachment_save_query_info_cb, save_context); +} + +GFile * +e_attachment_save_finish (EAttachment *attachment, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + GFile *destination; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), NULL); + g_return_val_if_fail (G_IS_SIMPLE_ASYNC_RESULT (result), NULL); + + simple = G_SIMPLE_ASYNC_RESULT (result); + destination = g_simple_async_result_get_op_res_gpointer (simple); + if (destination != NULL) + g_object_ref (destination); + g_simple_async_result_propagate_error (simple, error); + + attachment_set_saving (attachment, FALSE); + + return destination; +} + +void +e_attachment_save_handle_error (EAttachment *attachment, + GAsyncResult *result, + GtkWindow *parent) +{ + GFile *file; + GFileInfo *file_info; + GtkWidget *dialog; + const gchar *display_name; + const gchar *primary_text; + GError *error = NULL; + + g_return_if_fail (E_IS_ATTACHMENT (attachment)); + g_return_if_fail (G_IS_ASYNC_RESULT (result)); + g_return_if_fail (GTK_IS_WINDOW (parent)); + + file = e_attachment_save_finish (attachment, result, &error); + + if (file != NULL) { + g_object_unref (file); + return; + } + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + file_info = e_attachment_get_file_info (attachment); + + if (file_info != NULL) + display_name = g_file_info_get_display_name (file_info); + else + display_name = NULL; + + if (display_name != NULL) + primary_text = g_strdup_printf ( + _("Could not save '%s'"), display_name); + else + primary_text = g_strdup_printf ( + _("Could not save the attachment")); + + dialog = gtk_message_dialog_new_with_markup ( + parent, GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "<big><b>%s</b></big>", primary_text); + + gtk_message_dialog_format_secondary_text ( + GTK_MESSAGE_DIALOG (dialog), "%s", error->message); + + gtk_dialog_run (GTK_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + g_error_free (error); +} + +gboolean +e_attachment_save (EAttachment *attachment, + GFile *in_destination, + GFile **out_destination, + GError **error) +{ + EAsyncClosure *closure; + GAsyncResult *result; + + g_return_val_if_fail (E_IS_ATTACHMENT (attachment), FALSE); + g_return_val_if_fail (out_destination != NULL, FALSE); + + closure = e_async_closure_new (); + + e_attachment_save_async (attachment, in_destination, e_async_closure_callback, closure); + + result = e_async_closure_wait (closure); + + *out_destination = e_attachment_save_finish (attachment, result, error); + + e_async_closure_free (closure); + + return *out_destination != NULL; +} diff --git a/e-util/e-attachment.h b/e-util/e-attachment.h new file mode 100644 index 0000000000..268bcf68b9 --- /dev/null +++ b/e-util/e-attachment.h @@ -0,0 +1,159 @@ +/* + * e-attachment.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ATTACHMENT_H +#define E_ATTACHMENT_H + +#include <gtk/gtk.h> +#include <camel/camel.h> + +/* Standard GObject macros */ +#define E_TYPE_ATTACHMENT \ + (e_attachment_get_type ()) +#define E_ATTACHMENT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ATTACHMENT, EAttachment)) +#define E_ATTACHMENT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ATTACHMENT, EAttachmentClass)) +#define E_IS_ATTACHMENT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ATTACHMENT)) +#define E_IS_ATTACHMENT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ATTACHMENT)) +#define E_ATTACHMENT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ATTACHMENT, EAttachmentClass)) + +G_BEGIN_DECLS + +typedef struct _EAttachment EAttachment; +typedef struct _EAttachmentClass EAttachmentClass; +typedef struct _EAttachmentPrivate EAttachmentPrivate; + +struct _EAttachment { + GObject parent; + EAttachmentPrivate *priv; +}; + +struct _EAttachmentClass { + GObjectClass parent_class; +}; + +GType e_attachment_get_type (void); +EAttachment * e_attachment_new (void); +EAttachment * e_attachment_new_for_path (const gchar *path); +EAttachment * e_attachment_new_for_uri (const gchar *uri); +EAttachment * e_attachment_new_for_message (CamelMimeMessage *message); +void e_attachment_add_to_multipart (EAttachment *attachment, + CamelMultipart *multipart, + const gchar *default_charset); +void e_attachment_cancel (EAttachment *attachment); +gboolean e_attachment_get_can_show (EAttachment *attachment); +void e_attachment_set_can_show (EAttachment *attachment, + gboolean can_show); +const gchar * e_attachment_get_disposition (EAttachment *attachment); +void e_attachment_set_disposition (EAttachment *attachment, + const gchar *disposition); +GFile * e_attachment_get_file (EAttachment *attachment); +void e_attachment_set_file (EAttachment *attachment, + GFile *file); +GFileInfo * e_attachment_get_file_info (EAttachment *attachment); +void e_attachment_set_file_info (EAttachment *attachment, + GFileInfo *file_info); +gchar * e_attachment_get_mime_type (EAttachment *attachment); +GIcon * e_attachment_get_icon (EAttachment *attachment); +gboolean e_attachment_get_loading (EAttachment *attachment); +CamelMimePart * e_attachment_get_mime_part (EAttachment *attachment); +void e_attachment_set_mime_part (EAttachment *attachment, + CamelMimePart *mime_part); +gint e_attachment_get_percent (EAttachment *attachment); +GtkTreeRowReference * + e_attachment_get_reference (EAttachment *attachment); +void e_attachment_set_reference (EAttachment *attachment, + GtkTreeRowReference *reference); +gboolean e_attachment_get_saving (EAttachment *attachment); +gboolean e_attachment_get_shown (EAttachment *attachment); +void e_attachment_set_shown (EAttachment *attachment, + gboolean shown); +camel_cipher_validity_encrypt_t + e_attachment_get_encrypted (EAttachment *attachment); +void e_attachment_set_encrypted (EAttachment *attachment, + camel_cipher_validity_encrypt_t encrypted); +camel_cipher_validity_sign_t + e_attachment_get_signed (EAttachment *attachment); +void e_attachment_set_signed (EAttachment *attachment, + camel_cipher_validity_sign_t signed_); +const gchar * e_attachment_get_description (EAttachment *attachment); +const gchar * e_attachment_get_thumbnail_path (EAttachment *attachment); +gboolean e_attachment_is_rfc822 (EAttachment *attachment); +GList * e_attachment_list_apps (EAttachment *attachment); + +/* Asynchronous Operations */ +void e_attachment_load_async (EAttachment *attachment, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_attachment_load_finish (EAttachment *attachment, + GAsyncResult *result, + GError **error); +gboolean e_attachment_load (EAttachment *attachment, + GError **error); +void e_attachment_open_async (EAttachment *attachment, + GAppInfo *app_info, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_attachment_open_finish (EAttachment *attachment, + GAsyncResult *result, + GError **error); +gboolean e_attachment_open (EAttachment *attachment, + GAppInfo *app_info, + GError **error); +void e_attachment_save_async (EAttachment *attachment, + GFile *destination, + GAsyncReadyCallback callback, + gpointer user_data); +GFile * e_attachment_save_finish (EAttachment *attachment, + GAsyncResult *result, + GError **error); +gboolean e_attachment_save (EAttachment *attachment, + GFile *in_destination, + GFile **out_destination, + GError **error); + +/* Handy GAsyncReadyCallback Functions */ +void e_attachment_load_handle_error (EAttachment *attachment, + GAsyncResult *result, + GtkWindow *parent); +void e_attachment_open_handle_error (EAttachment *attachment, + GAsyncResult *result, + GtkWindow *parent); +void e_attachment_save_handle_error (EAttachment *attachment, + GAsyncResult *result, + GtkWindow *parent); + +G_END_DECLS + +#endif /* E_ATTACHMENT_H */ diff --git a/e-util/e-auth-combo-box.c b/e-util/e-auth-combo-box.c new file mode 100644 index 0000000000..bd3d8c78ea --- /dev/null +++ b/e-util/e-auth-combo-box.c @@ -0,0 +1,266 @@ +/* + * e-auth-combo-box.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-auth-combo-box.h" + +#define E_AUTH_COMBO_BOX_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_AUTH_COMBO_BOX, EAuthComboBoxPrivate)) + +struct _EAuthComboBoxPrivate { + CamelProvider *provider; +}; + +enum { + PROP_0, + PROP_PROVIDER +}; + +enum { + COLUMN_MECHANISM, + COLUMN_DISPLAY_NAME, + COLUMN_STRIKETHROUGH, + COLUMN_AUTHTYPE, + NUM_COLUMNS +}; + +G_DEFINE_TYPE ( + EAuthComboBox, + e_auth_combo_box, + GTK_TYPE_COMBO_BOX) + +static void +auth_combo_box_rebuild_model (EAuthComboBox *combo_box) +{ + GtkComboBox *gtk_combo_box; + CamelProvider *provider; + GtkTreeModel *model; + GList *link; + const gchar *active_id; + + provider = e_auth_combo_box_get_provider (combo_box); + + gtk_combo_box = GTK_COMBO_BOX (combo_box); + model = gtk_combo_box_get_model (gtk_combo_box); + active_id = gtk_combo_box_get_active_id (gtk_combo_box); + + gtk_list_store_clear (GTK_LIST_STORE (model)); + + if (provider == NULL) + return; + + for (link = provider->authtypes; link != NULL; link = link->next) { + CamelServiceAuthType *authtype = link->data; + GtkTreeIter iter; + + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + COLUMN_MECHANISM, authtype->authproto, + COLUMN_DISPLAY_NAME, authtype->name, + COLUMN_AUTHTYPE, authtype, + -1); + } + + /* Try selecting the previous mechanism. */ + if (active_id != NULL) + gtk_combo_box_set_active_id (gtk_combo_box, active_id); + + /* Or else fall back to the first mechanism. */ + if (gtk_combo_box_get_active (gtk_combo_box) == -1) + gtk_combo_box_set_active (gtk_combo_box, 0); +} + +static void +auth_combo_box_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_PROVIDER: + e_auth_combo_box_set_provider ( + E_AUTH_COMBO_BOX (object), + g_value_get_pointer (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +auth_combo_box_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_PROVIDER: + g_value_set_pointer ( + value, + e_auth_combo_box_get_provider ( + E_AUTH_COMBO_BOX (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +auth_combo_box_constructed (GObject *object) +{ + GtkComboBox *combo_box; + GtkListStore *list_store; + GtkCellLayout *cell_layout; + GtkCellRenderer *cell_renderer; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_auth_combo_box_parent_class)->constructed (object); + + list_store = gtk_list_store_new ( + NUM_COLUMNS, + G_TYPE_STRING, /* COLUMN_MECHANISM */ + G_TYPE_STRING, /* COLUMN_DISPLAY_NAME */ + G_TYPE_BOOLEAN, /* COLUMN_STRIKETHROUGH */ + G_TYPE_POINTER); /* COLUMN_AUTHTYPE */ + + combo_box = GTK_COMBO_BOX (object); + gtk_combo_box_set_model (combo_box, GTK_TREE_MODEL (list_store)); + gtk_combo_box_set_id_column (combo_box, COLUMN_MECHANISM); + g_object_unref (list_store); + + cell_layout = GTK_CELL_LAYOUT (object); + cell_renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (cell_layout, cell_renderer, TRUE); + + gtk_cell_layout_set_attributes ( + cell_layout, cell_renderer, + "text", COLUMN_DISPLAY_NAME, + "strikethrough", COLUMN_STRIKETHROUGH, + NULL); +} + +static void +e_auth_combo_box_class_init (EAuthComboBoxClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EAuthComboBoxPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = auth_combo_box_set_property; + object_class->get_property = auth_combo_box_get_property; + object_class->constructed = auth_combo_box_constructed; + + g_object_class_install_property ( + object_class, + PROP_PROVIDER, + g_param_spec_pointer ( + "provider", + "Provider", + "The provider to query for auth mechanisms", + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_auth_combo_box_init (EAuthComboBox *combo_box) +{ + combo_box->priv = E_AUTH_COMBO_BOX_GET_PRIVATE (combo_box); +} + +GtkWidget * +e_auth_combo_box_new (void) +{ + return g_object_new (E_TYPE_AUTH_COMBO_BOX, NULL); +} + +CamelProvider * +e_auth_combo_box_get_provider (EAuthComboBox *combo_box) +{ + g_return_val_if_fail (E_IS_AUTH_COMBO_BOX (combo_box), NULL); + + return combo_box->priv->provider; +} + +void +e_auth_combo_box_set_provider (EAuthComboBox *combo_box, + CamelProvider *provider) +{ + g_return_if_fail (E_IS_AUTH_COMBO_BOX (combo_box)); + + if (provider == combo_box->priv->provider) + return; + + combo_box->priv->provider = provider; + + g_object_notify (G_OBJECT (combo_box), "provider"); + + auth_combo_box_rebuild_model (combo_box); +} + +void +e_auth_combo_box_update_available (EAuthComboBox *combo_box, + GList *available_authtypes) +{ + GtkComboBox *gtk_combo_box; + GtkTreeModel *model; + GtkTreeIter iter; + gint active_index; + gint available_index = -1; + gint index = 0; + gboolean iter_set; + + g_return_if_fail (E_IS_AUTH_COMBO_BOX (combo_box)); + + gtk_combo_box = GTK_COMBO_BOX (combo_box); + model = gtk_combo_box_get_model (gtk_combo_box); + active_index = gtk_combo_box_get_active (gtk_combo_box); + + iter_set = gtk_tree_model_get_iter_first (model, &iter); + + while (iter_set) { + CamelServiceAuthType *authtype; + gboolean available; + + gtk_tree_model_get ( + model, &iter, COLUMN_AUTHTYPE, &authtype, -1); + + available = (g_list_find ( + available_authtypes, authtype) != NULL); + + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + COLUMN_STRIKETHROUGH, !available, -1); + + if (index == active_index && !available) + active_index = -1; + + if (available && available_index == -1) + available_index = index; + + iter_set = gtk_tree_model_iter_next (model, &iter); + index++; + } + + /* If the active combo_box item turned out to be unavailable + * (or there was no active item), select the first available. */ + if (active_index == -1 && available_index != -1) + gtk_combo_box_set_active (gtk_combo_box, available_index); +} diff --git a/e-util/e-auth-combo-box.h b/e-util/e-auth-combo-box.h new file mode 100644 index 0000000000..a8ec3f9bf6 --- /dev/null +++ b/e-util/e-auth-combo-box.h @@ -0,0 +1,75 @@ +/* + * e-auth-combo-box.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_AUTH_COMBO_BOX_H +#define E_AUTH_COMBO_BOX_H + +#include <gtk/gtk.h> +#include <camel/camel.h> + +/* Standard GObject macros */ +#define E_TYPE_AUTH_COMBO_BOX \ + (e_auth_combo_box_get_type ()) +#define E_AUTH_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_AUTH_COMBO_BOX, EAuthComboBox)) +#define E_AUTH_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_AUTH_COMBO_BOX, EAuthComboBoxClass)) +#define E_IS_AUTH_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_AUTH_COMBO_BOX)) +#define E_IS_AUTH_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_AUTH_COMBO_BOX)) +#define E_AUTH_COMBO_BOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_AUTH_COMBO_BOX, EAuthComboBoxClass)) + +G_BEGIN_DECLS + +typedef struct _EAuthComboBox EAuthComboBox; +typedef struct _EAuthComboBoxClass EAuthComboBoxClass; +typedef struct _EAuthComboBoxPrivate EAuthComboBoxPrivate; + +struct _EAuthComboBox { + GtkComboBox parent; + EAuthComboBoxPrivate *priv; +}; + +struct _EAuthComboBoxClass { + GtkComboBoxClass parent_class; +}; + +GType e_auth_combo_box_get_type (void) G_GNUC_CONST; +GtkWidget * e_auth_combo_box_new (void); +CamelProvider * e_auth_combo_box_get_provider (EAuthComboBox *combo_box); +void e_auth_combo_box_set_provider (EAuthComboBox *combo_box, + CamelProvider *provider); +void e_auth_combo_box_update_available + (EAuthComboBox *combo_box, + GList *available_authtypes); + +G_END_DECLS + +#endif /* E_AUTH_COMBO_BOX_H */ + diff --git a/e-util/e-autocomplete-selector.c b/e-util/e-autocomplete-selector.c new file mode 100644 index 0000000000..c0bf207bbd --- /dev/null +++ b/e-util/e-autocomplete-selector.c @@ -0,0 +1,96 @@ +/* + * e-autocomplete-selector.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-autocomplete-selector.h" + +G_DEFINE_TYPE ( + EAutocompleteSelector, + e_autocomplete_selector, + E_TYPE_SOURCE_SELECTOR) + +static gboolean +autocomplete_selector_get_source_selected (ESourceSelector *selector, + ESource *source) +{ + ESourceAutocomplete *extension; + const gchar *extension_name; + + /* Make sure this source is an address book. */ + extension_name = e_source_selector_get_extension_name (selector); + if (!e_source_has_extension (source, extension_name)) + return FALSE; + + extension_name = E_SOURCE_EXTENSION_AUTOCOMPLETE; + extension = e_source_get_extension (source, extension_name); + g_return_val_if_fail (E_IS_SOURCE_AUTOCOMPLETE (extension), FALSE); + + return e_source_autocomplete_get_include_me (extension); +} + +static void +autocomplete_selector_set_source_selected (ESourceSelector *selector, + ESource *source, + gboolean selected) +{ + ESourceAutocomplete *extension; + const gchar *extension_name; + + /* Make sure this source is an address book. */ + extension_name = e_source_selector_get_extension_name (selector); + if (!e_source_has_extension (source, extension_name)) + return; + + extension_name = E_SOURCE_EXTENSION_AUTOCOMPLETE; + extension = e_source_get_extension (source, extension_name); + g_return_if_fail (E_IS_SOURCE_AUTOCOMPLETE (extension)); + + if (selected != e_source_autocomplete_get_include_me (extension)) { + e_source_autocomplete_set_include_me (extension, selected); + e_source_selector_queue_write (selector, source); + } +} + +static void +e_autocomplete_selector_class_init (EAutocompleteSelectorClass *class) +{ + ESourceSelectorClass *source_selector_class; + + source_selector_class = E_SOURCE_SELECTOR_CLASS (class); + source_selector_class->get_source_selected = + autocomplete_selector_get_source_selected; + source_selector_class->set_source_selected = + autocomplete_selector_set_source_selected; +} + +static void +e_autocomplete_selector_init (EAutocompleteSelector *selector) +{ + e_source_selector_set_show_colors ( + E_SOURCE_SELECTOR (selector), FALSE); +} + +GtkWidget * +e_autocomplete_selector_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_AUTOCOMPLETE_SELECTOR, + "extension-name", E_SOURCE_EXTENSION_ADDRESS_BOOK, + "registry", registry, NULL); +} diff --git a/e-util/e-autocomplete-selector.h b/e-util/e-autocomplete-selector.h new file mode 100644 index 0000000000..af17f21828 --- /dev/null +++ b/e-util/e-autocomplete-selector.h @@ -0,0 +1,68 @@ +/* + * e-autocomplete-selector.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_AUTOCOMPLETE_SELECTOR_H +#define E_AUTOCOMPLETE_SELECTOR_H + +#include <e-util/e-source-selector.h> + +/* Standard GObject macros */ +#define E_TYPE_AUTOCOMPLETE_SELECTOR \ + (e_autocomplete_selector_get_type ()) +#define E_AUTOCOMPLETE_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_AUTOCOMPLETE_SELECTOR, EAutocompleteSelector)) +#define E_AUTOCOMPLETE_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_AUTOCOMPLETE_SELECTOR, EAutocompleteSelectorClass)) +#define E_IS_AUTOCOMPLETE_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_AUTOCOMPLETE_SELECTOR)) +#define E_IS_AUTOCOMPLETE_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_AUTOCOMPLETE_SELECTOR)) +#define E_AUTOCOMPLETE_SELECTOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_AUTOCOMPLETE_SELECTOR, EAutocompleteSelectorClass)) + +G_BEGIN_DECLS + +typedef struct _EAutocompleteSelector EAutocompleteSelector; +typedef struct _EAutocompleteSelectorClass EAutocompleteSelectorClass; +typedef struct _EAutocompleteSelectorPrivate EAutocompleteSelectorPrivate; + +struct _EAutocompleteSelector { + ESourceSelector parent; + EAutocompleteSelectorPrivate *priv; +}; + +struct _EAutocompleteSelectorClass { + ESourceSelectorClass parent_class; +}; + +GType e_autocomplete_selector_get_type + (void) G_GNUC_CONST; +GtkWidget * e_autocomplete_selector_new (ESourceRegistry *registry); + +G_END_DECLS + +#endif /* E_AUTOCOMPLETE_SELECTOR_H */ diff --git a/e-util/e-bit-array.c b/e-util/e-bit-array.c index 456a4d495d..b17f8a089b 100644 --- a/e-util/e-bit-array.c +++ b/e-util/e-bit-array.c @@ -28,7 +28,6 @@ #include <gtk/gtk.h> #include "e-bit-array.h" -#include "e-util.h" #define ONES ((guint32) 0xffffffff) diff --git a/e-util/e-bit-array.h b/e-util/e-bit-array.h index 717fe585e7..39b55d906c 100644 --- a/e-util/e-bit-array.h +++ b/e-util/e-bit-array.h @@ -21,6 +21,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef _E_BIT_ARRAY_H_ #define _E_BIT_ARRAY_H_ diff --git a/e-util/e-book-source-config.c b/e-util/e-book-source-config.c new file mode 100644 index 0000000000..56d9771d9f --- /dev/null +++ b/e-util/e-book-source-config.c @@ -0,0 +1,287 @@ +/* + * e-book-source-config.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-book-source-config.h" + +#include <config.h> +#include <glib/gi18n-lib.h> + +#define E_BOOK_SOURCE_CONFIG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_BOOK_SOURCE_CONFIG, EBookSourceConfigPrivate)) + +struct _EBookSourceConfigPrivate { + GtkWidget *default_button; + GtkWidget *autocomplete_button; +}; + +G_DEFINE_TYPE ( + EBookSourceConfig, + e_book_source_config, + E_TYPE_SOURCE_CONFIG) + +static ESource * +book_source_config_ref_default (ESourceConfig *config) +{ + ESourceRegistry *registry; + + registry = e_source_config_get_registry (config); + + return e_source_registry_ref_default_address_book (registry); +} + +static void +book_source_config_set_default (ESourceConfig *config, + ESource *source) +{ + ESourceRegistry *registry; + + registry = e_source_config_get_registry (config); + + e_source_registry_set_default_address_book (registry, source); +} + +static void +book_source_config_dispose (GObject *object) +{ + EBookSourceConfigPrivate *priv; + + priv = E_BOOK_SOURCE_CONFIG_GET_PRIVATE (object); + + if (priv->default_button != NULL) { + g_object_unref (priv->default_button); + priv->default_button = NULL; + } + + if (priv->autocomplete_button != NULL) { + g_object_unref (priv->autocomplete_button); + priv->autocomplete_button = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_book_source_config_parent_class)->dispose (object); +} + +static void +book_source_config_constructed (GObject *object) +{ + EBookSourceConfigPrivate *priv; + ESource *default_source; + ESource *original_source; + ESourceConfig *config; + GObjectClass *class; + GtkWidget *widget; + const gchar *label; + + /* Chain up to parent's constructed() method. */ + class = G_OBJECT_CLASS (e_book_source_config_parent_class); + class->constructed (object); + + config = E_SOURCE_CONFIG (object); + priv = E_BOOK_SOURCE_CONFIG_GET_PRIVATE (object); + + label = _("Mark as default address book"); + widget = gtk_check_button_new_with_label (label); + priv->default_button = g_object_ref_sink (widget); + gtk_widget_show (widget); + + label = _("Autocomplete with this address book"); + widget = gtk_check_button_new_with_label (label); + priv->autocomplete_button = g_object_ref_sink (widget); + gtk_widget_show (widget); + + default_source = book_source_config_ref_default (config); + original_source = e_source_config_get_original_source (config); + + if (original_source != NULL) { + gboolean active; + + active = e_source_equal (original_source, default_source); + g_object_set (priv->default_button, "active", active, NULL); + } + + g_object_unref (default_source); + + e_source_config_insert_widget ( + config, NULL, NULL, priv->default_button); + + e_source_config_insert_widget ( + config, NULL, NULL, priv->autocomplete_button); +} + +static const gchar * +book_source_config_get_backend_extension_name (ESourceConfig *config) +{ + return E_SOURCE_EXTENSION_ADDRESS_BOOK; +} + +static GList * +book_source_config_list_eligible_collections (ESourceConfig *config) +{ + GQueue trash = G_QUEUE_INIT; + GList *list, *link; + + /* Chain up to parent's list_eligible_collections() method. */ + list = E_SOURCE_CONFIG_CLASS (e_book_source_config_parent_class)-> + list_eligible_collections (config); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESource *source = E_SOURCE (link->data); + ESourceCollection *extension; + const gchar *extension_name; + + extension_name = E_SOURCE_EXTENSION_COLLECTION; + extension = e_source_get_extension (source, extension_name); + + if (!e_source_collection_get_contacts_enabled (extension)) + g_queue_push_tail (&trash, link); + } + + /* Remove ineligible collections from the list. */ + while ((link = g_queue_pop_head (&trash)) != NULL) { + g_object_unref (link->data); + list = g_list_delete_link (list, link); + } + + return list; +} + +static void +book_source_config_init_candidate (ESourceConfig *config, + ESource *scratch_source) +{ + EBookSourceConfigPrivate *priv; + ESourceConfigClass *class; + ESourceExtension *extension; + const gchar *extension_name; + + /* Chain up to parent's init_candidate() method. */ + class = E_SOURCE_CONFIG_CLASS (e_book_source_config_parent_class); + class->init_candidate (config, scratch_source); + + priv = E_BOOK_SOURCE_CONFIG_GET_PRIVATE (config); + + extension_name = E_SOURCE_EXTENSION_AUTOCOMPLETE; + extension = e_source_get_extension (scratch_source, extension_name); + + g_object_bind_property ( + extension, "include-me", + priv->autocomplete_button, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); +} + +static void +book_source_config_commit_changes (ESourceConfig *config, + ESource *scratch_source) +{ + EBookSourceConfigPrivate *priv; + ESourceConfigClass *class; + ESource *default_source; + GtkToggleButton *toggle_button; + + priv = E_BOOK_SOURCE_CONFIG_GET_PRIVATE (config); + toggle_button = GTK_TOGGLE_BUTTON (priv->default_button); + + /* Chain up to parent's commit_changes() method. */ + class = E_SOURCE_CONFIG_CLASS (e_book_source_config_parent_class); + class->commit_changes (config, scratch_source); + + default_source = book_source_config_ref_default (config); + + /* The default setting is a little tricky to get right. If + * the toggle button is active, this ESource is now the default. + * That much is simple. But if the toggle button is NOT active, + * then we have to inspect the old default. If this ESource WAS + * the default, reset the default to 'system'. If this ESource + * WAS NOT the old default, leave it alone. */ + if (gtk_toggle_button_get_active (toggle_button)) + book_source_config_set_default (config, scratch_source); + else if (e_source_equal (scratch_source, default_source)) + book_source_config_set_default (config, NULL); + + g_object_unref (default_source); +} + +static void +e_book_source_config_class_init (EBookSourceConfigClass *class) +{ + GObjectClass *object_class; + ESourceConfigClass *source_config_class; + + g_type_class_add_private (class, sizeof (EBookSourceConfigPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = book_source_config_dispose; + object_class->constructed = book_source_config_constructed; + + source_config_class = E_SOURCE_CONFIG_CLASS (class); + source_config_class->get_backend_extension_name = + book_source_config_get_backend_extension_name; + source_config_class->list_eligible_collections = + book_source_config_list_eligible_collections; + source_config_class->init_candidate = book_source_config_init_candidate; + source_config_class->commit_changes = book_source_config_commit_changes; +} + +static void +e_book_source_config_init (EBookSourceConfig *config) +{ + config->priv = E_BOOK_SOURCE_CONFIG_GET_PRIVATE (config); +} + +GtkWidget * +e_book_source_config_new (ESourceRegistry *registry, + ESource *original_source) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + if (original_source != NULL) + g_return_val_if_fail (E_IS_SOURCE (original_source), NULL); + + return g_object_new ( + E_TYPE_BOOK_SOURCE_CONFIG, "registry", registry, + "original-source", original_source, NULL); +} + +void +e_book_source_config_add_offline_toggle (EBookSourceConfig *config, + ESource *scratch_source) +{ + GtkWidget *widget; + ESourceExtension *extension; + const gchar *extension_name; + + g_return_if_fail (E_IS_BOOK_SOURCE_CONFIG (config)); + g_return_if_fail (E_IS_SOURCE (scratch_source)); + + extension_name = E_SOURCE_EXTENSION_OFFLINE; + extension = e_source_get_extension (scratch_source, extension_name); + + widget = gtk_check_button_new_with_label ( + _("Copy book content locally for offline operation")); + e_source_config_insert_widget ( + E_SOURCE_CONFIG (config), scratch_source, NULL, widget); + gtk_widget_show (widget); + + g_object_bind_property ( + extension, "stay-synchronized", + widget, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); +} diff --git a/e-util/e-book-source-config.h b/e-util/e-book-source-config.h new file mode 100644 index 0000000000..3e000789e9 --- /dev/null +++ b/e-util/e-book-source-config.h @@ -0,0 +1,71 @@ +/* + * e-book-source-config.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_BOOK_SOURCE_CONFIG_H +#define E_BOOK_SOURCE_CONFIG_H + +#include <e-util/e-source-config.h> + +/* Standard GObject macros */ +#define E_TYPE_BOOK_SOURCE_CONFIG \ + (e_book_source_config_get_type ()) +#define E_BOOK_SOURCE_CONFIG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_BOOK_SOURCE_CONFIG, EBookSourceConfig)) +#define E_BOOK_SOURCE_CONFIG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_BOOK_SOURCE_CONFIG, EBookSourceConfigClass)) +#define E_IS_BOOK_SOURCE_CONFIG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_BOOK_SOURCE_CONFIG)) +#define E_IS_BOOK_SOURCE_CONFIG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_BOOK_SOURCE_CONFIG)) +#define E_BOOK_SOURCE_CONFIG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_BOOK_SOURCE_CONFIG, EBookSourceConfigClass)) + +G_BEGIN_DECLS + +typedef struct _EBookSourceConfig EBookSourceConfig; +typedef struct _EBookSourceConfigClass EBookSourceConfigClass; +typedef struct _EBookSourceConfigPrivate EBookSourceConfigPrivate; + +struct _EBookSourceConfig { + ESourceConfig parent; + EBookSourceConfigPrivate *priv; +}; + +struct _EBookSourceConfigClass { + ESourceConfigClass parent_class; +}; + +GType e_book_source_config_get_type (void) G_GNUC_CONST; +GtkWidget * e_book_source_config_new (ESourceRegistry *registry, + ESource *original_source); +void e_book_source_config_add_offline_toggle + (EBookSourceConfig *config, + ESource *scratch_source); + +G_END_DECLS + +#endif /* E_BOOK_SOURCE_CONFIG_H */ diff --git a/e-util/e-buffer-tagger.c b/e-util/e-buffer-tagger.c new file mode 100644 index 0000000000..c05a854020 --- /dev/null +++ b/e-util/e-buffer-tagger.c @@ -0,0 +1,692 @@ +/* + * e-buffer-tagger.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n.h> +#include <regex.h> +#include <string.h> +#include <ctype.h> + +#include "e-buffer-tagger.h" + +#include "e-misc-utils.h" + +enum EBufferTaggerState +{ + E_BUFFER_TAGGER_STATE_NONE = 0, + E_BUFFER_TAGGER_STATE_INSDEL = 1 << 0, /* set when was called insert or delete of a text */ + E_BUFFER_TAGGER_STATE_CHANGED = 1 << 1, /* remark of the buffer is scheduled */ + E_BUFFER_TAGGER_STATE_IS_HOVERING = 1 << 2, /* mouse is over the link */ + E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP = 1 << 3, /* mouse is over the link and the tooltip can be shown */ + E_BUFFER_TAGGER_STATE_CTRL_DOWN = 1 << 4 /* Ctrl key is down */ +}; + +#define E_BUFFER_TAGGER_DATA_STATE "EBufferTagger::state" +#define E_BUFFER_TAGGER_LINK_TAG "EBufferTagger::link" + +struct _MagicInsertMatch +{ + const gchar *regex; + regex_t *preg; + const gchar *prefix; +}; + +typedef struct _MagicInsertMatch MagicInsertMatch; + +static MagicInsertMatch mim[] = { + /* prefixed expressions */ + { "(news|telnet|nntp|file|http|ftp|sftp|https|webcal)://([-a-z0-9]+(:[-a-z0-9]+)?@)?[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-a-z0-9_$.+!*(),;:@%&=?/~#']*[^]'.}>\\) \n\r\t,?!;:\"]?)?", NULL, NULL }, + { "(sip|h323|callto):([-_a-z0-9.'\\+]+(:[0-9]{1,5})?(/[-_a-z0-9.']+)?)(@([-_a-z0-9.%=?]+|([0-9]{1,3}.){3}[0-9]{1,3})?)?(:[0-9]{1,5})?", NULL, NULL }, + { "mailto:[-_a-z0-9.'\\+]+@[-_a-z0-9.%=?]+", NULL, NULL }, + /* not prefixed expression */ + { "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) \n\r\t,?!;:\"]?)?", NULL, "http://" }, + { "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) \n\r\t,?!;:\"]?)?", NULL, "ftp://" }, + { "[-_a-z0-9.'\\+]+@[-_a-z0-9.%=?]+", NULL, "mailto:" } +}; + +static void +init_magic_links (void) +{ + static gboolean done = FALSE; + gint i; + + if (done) + return; + + done = TRUE; + + for (i = 0; i < G_N_ELEMENTS (mim); i++) { + mim[i].preg = g_new0 (regex_t, 1); + if (regcomp (mim[i].preg, mim[i].regex, REG_EXTENDED | REG_ICASE)) { + /* error */ + g_free (mim[i].preg); + mim[i].preg = 0; + } + } +} + +static void +markup_text (GtkTextBuffer *buffer) +{ + GtkTextIter start, end; + gchar *text; + gint i; + regmatch_t pmatch[2]; + gboolean any; + const gchar *str; + gint offset = 0; + + g_return_if_fail (buffer != NULL); + + gtk_text_buffer_get_start_iter (buffer, &start); + gtk_text_buffer_get_end_iter (buffer, &end); + gtk_text_buffer_remove_tag_by_name (buffer, E_BUFFER_TAGGER_LINK_TAG, &start, &end); + text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE); + + str = text; + any = TRUE; + while (any) { + any = FALSE; + for (i = 0; i < G_N_ELEMENTS (mim); i++) { + if (mim[i].preg && !regexec (mim[i].preg, str, 2, pmatch, 0)) { + gtk_text_buffer_get_iter_at_offset (buffer, &start, offset + pmatch[0].rm_so); + gtk_text_buffer_get_iter_at_offset (buffer, &end, offset + pmatch[0].rm_eo); + gtk_text_buffer_apply_tag_by_name (buffer, E_BUFFER_TAGGER_LINK_TAG, &start, &end); + + any = TRUE; + str += pmatch[0].rm_eo; + offset += pmatch[0].rm_eo; + break; + } + } + } + + g_free (text); +} + +static void +get_pointer_position (GtkTextView *text_view, + gint *x, + gint *y) +{ + GdkWindow *window; + GdkDisplay *display; + GdkDeviceManager *device_manager; + GdkDevice *device; + + window = gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_WIDGET); + display = gdk_window_get_display (window); + device_manager = gdk_display_get_device_manager (display); + device = gdk_device_manager_get_client_pointer (device_manager); + + gdk_window_get_device_position (window, device, x, y, NULL); +} + +static guint32 +get_state (GtkTextBuffer *buffer) +{ + g_return_val_if_fail (buffer != NULL, E_BUFFER_TAGGER_STATE_NONE); + g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), E_BUFFER_TAGGER_STATE_NONE); + + return GPOINTER_TO_INT (g_object_get_data (G_OBJECT (buffer), E_BUFFER_TAGGER_DATA_STATE)); +} + +static void +set_state (GtkTextBuffer *buffer, + guint32 state) +{ + g_object_set_data (G_OBJECT (buffer), E_BUFFER_TAGGER_DATA_STATE, GINT_TO_POINTER (state)); +} + +static void +update_state (GtkTextBuffer *buffer, + guint32 value, + gboolean do_set) +{ + guint32 state; + + g_return_if_fail (buffer != NULL); + g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer)); + + state = get_state (buffer); + + if (do_set) + state = state | value; + else + state = state & (~value); + + set_state (buffer, state); +} + +static gboolean +get_tag_bounds (GtkTextIter *iter, + GtkTextTag *tag, + GtkTextIter *start, + GtkTextIter *end) +{ + gboolean res = FALSE; + + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (tag != NULL, FALSE); + g_return_val_if_fail (start != NULL, FALSE); + g_return_val_if_fail (end != NULL, FALSE); + + if (gtk_text_iter_has_tag (iter, tag)) { + *start = *iter; + *end = *iter; + + if (!gtk_text_iter_begins_tag (start, tag)) + gtk_text_iter_backward_to_tag_toggle (start, tag); + + if (!gtk_text_iter_ends_tag (end, tag)) + gtk_text_iter_forward_to_tag_toggle (end, tag); + + res = TRUE; + } + + return res; +} + +static gchar * +get_url_at_iter (GtkTextBuffer *buffer, + GtkTextIter *iter) +{ + GtkTextTagTable *tag_table; + GtkTextTag *tag; + GtkTextIter start, end; + gchar *url = NULL; + + g_return_val_if_fail (buffer != NULL, NULL); + + tag_table = gtk_text_buffer_get_tag_table (buffer); + tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG); + g_return_val_if_fail (tag != NULL, FALSE); + + if (get_tag_bounds (iter, tag, &start, &end)) + url = gtk_text_iter_get_text (&start, &end); + + return url; +} + +static gboolean +invoke_link_if_present (GtkTextBuffer *buffer, + GtkTextIter *iter) +{ + gboolean res; + gchar *url; + + g_return_val_if_fail (buffer != NULL, FALSE); + + url = get_url_at_iter (buffer, iter); + + res = url && *url; + if (res) + e_show_uri (NULL, url); + + g_free (url); + + return res; +} + +static void +remove_tag_if_present (GtkTextBuffer *buffer, + GtkTextIter *where) +{ + GtkTextTagTable *tag_table; + GtkTextTag *tag; + GtkTextIter start, end; + + g_return_if_fail (buffer != NULL); + g_return_if_fail (where != NULL); + + tag_table = gtk_text_buffer_get_tag_table (buffer); + tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG); + g_return_if_fail (tag != NULL); + + if (get_tag_bounds (where, tag, &start, &end)) + gtk_text_buffer_remove_tag (buffer, tag, &start, &end); +} + +static void +buffer_insert_text (GtkTextBuffer *buffer, + GtkTextIter *location, + gchar *text, + gint len, + gpointer user_data) +{ + update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL, TRUE); + remove_tag_if_present (buffer, location); +} + +static void +buffer_delete_range (GtkTextBuffer *buffer, + GtkTextIter *start, + GtkTextIter *end, + gpointer user_data) +{ + update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL, TRUE); + remove_tag_if_present (buffer, start); + remove_tag_if_present (buffer, end); +} + +static void +buffer_cursor_position (GtkTextBuffer *buffer, + gpointer user_data) +{ + guint32 state; + + state = get_state (buffer); + if (state & E_BUFFER_TAGGER_STATE_INSDEL) { + state = (state & (~E_BUFFER_TAGGER_STATE_INSDEL)) | E_BUFFER_TAGGER_STATE_CHANGED; + } else { + if (state & E_BUFFER_TAGGER_STATE_CHANGED) { + markup_text (buffer); + } + + state = state & (~ (E_BUFFER_TAGGER_STATE_CHANGED | E_BUFFER_TAGGER_STATE_INSDEL)); + } + + set_state (buffer, state); +} + +static void +update_mouse_cursor (GtkTextView *text_view, + gint x, + gint y) +{ + static GdkCursor *hand_cursor = NULL; + static GdkCursor *regular_cursor = NULL; + gboolean hovering = FALSE, hovering_over_link = FALSE, hovering_real; + guint32 state; + GtkTextBuffer *buffer = gtk_text_view_get_buffer (text_view); + GtkTextTagTable *tag_table; + GtkTextTag *tag; + GtkTextIter iter; + + if (!hand_cursor) { + hand_cursor = gdk_cursor_new (GDK_HAND2); + regular_cursor = gdk_cursor_new (GDK_XTERM); + } + + g_return_if_fail (buffer != NULL); + + tag_table = gtk_text_buffer_get_tag_table (buffer); + tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG); + g_return_if_fail (tag != NULL); + + state = get_state (buffer); + + gtk_text_view_get_iter_at_location (text_view, &iter, x, y); + hovering_real = gtk_text_iter_has_tag (&iter, tag); + + hovering_over_link = (state & E_BUFFER_TAGGER_STATE_IS_HOVERING) != 0; + if ((state & E_BUFFER_TAGGER_STATE_CTRL_DOWN) == 0) { + hovering = FALSE; + } else { + hovering = hovering_real; + } + + if (hovering != hovering_over_link) { + update_state (buffer, E_BUFFER_TAGGER_STATE_IS_HOVERING, hovering); + + if (hovering && gtk_widget_has_focus (GTK_WIDGET (text_view))) + gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), hand_cursor); + else + gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), regular_cursor); + + /* XXX Is this necessary? Appears to be a no-op. */ + get_pointer_position (text_view, NULL, NULL); + } + + hovering_over_link = (state & E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP) != 0; + + if (hovering_real != hovering_over_link) { + update_state (buffer, E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP, hovering_real); + + gtk_widget_trigger_tooltip_query (GTK_WIDGET (text_view)); + } +} + +static gboolean +textview_query_tooltip (GtkTextView *text_view, + gint x, + gint y, + gboolean keyboard_mode, + GtkTooltip *tooltip, + gpointer user_data) +{ + GtkTextBuffer *buffer; + guint32 state; + gboolean res = FALSE; + + if (keyboard_mode) + return FALSE; + + buffer = gtk_text_view_get_buffer (text_view); + g_return_val_if_fail (buffer != NULL, FALSE); + + state = get_state (buffer); + + if ((state & E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP) != 0) { + gchar *url; + GtkTextIter iter; + + gtk_text_view_window_to_buffer_coords ( + text_view, + GTK_TEXT_WINDOW_WIDGET, + x, y, &x, &y); + gtk_text_view_get_iter_at_location (text_view, &iter, x, y); + + url = get_url_at_iter (buffer, &iter); + res = url && *url; + + if (res) { + gchar *str; + + /* To Translators: The text is concatenated to a form: "Ctrl-click to open a link http://www.example.com" */ + str = g_strconcat (_("Ctrl-click to open a link"), " ", url, NULL); + gtk_tooltip_set_text (tooltip, str); + g_free (str); + } + + g_free (url); + } + + return res; +} + +/* Links can be activated by pressing Enter. */ +static gboolean +textview_key_press_event (GtkWidget *text_view, + GdkEventKey *event) +{ + GtkTextIter iter; + GtkTextBuffer *buffer; + + if ((event->state & GDK_CONTROL_MASK) == 0) + return FALSE; + + switch (event->keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); + gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer)); + if (invoke_link_if_present (buffer, &iter)) + return TRUE; + break; + + default: + break; + } + + return FALSE; +} + +static void +update_ctrl_state (GtkTextView *textview, + gboolean ctrl_is_down) +{ + GtkTextBuffer *buffer; + gint x, y; + + buffer = gtk_text_view_get_buffer (textview); + if (buffer) { + if (((get_state (buffer) & E_BUFFER_TAGGER_STATE_CTRL_DOWN) != 0) != (ctrl_is_down != FALSE)) { + update_state (buffer, E_BUFFER_TAGGER_STATE_CTRL_DOWN, ctrl_is_down != FALSE); + } + + get_pointer_position (textview, &x, &y); + gtk_text_view_window_to_buffer_coords (textview, GTK_TEXT_WINDOW_WIDGET, x, y, &x, &y); + update_mouse_cursor (textview, x, y); + } +} + +/* Links can also be activated by clicking. */ +static gboolean +textview_event_after (GtkTextView *textview, + GdkEvent *event) +{ + GtkTextIter start, end, iter; + GtkTextBuffer *buffer; + gint x, y; + GdkModifierType mt = 0; + guint event_button = 0; + gdouble event_x_win = 0; + gdouble event_y_win = 0; + + g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE); + + if (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) { + guint event_keyval = 0; + + gdk_event_get_keyval (event, &event_keyval); + + switch (event_keyval) { + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + update_ctrl_state ( + textview, + event->type == GDK_KEY_PRESS); + break; + } + + return FALSE; + } + + if (!gdk_event_get_state (event, &mt)) { + GdkWindow *window; + GdkDisplay *display; + GdkDeviceManager *device_manager; + GdkDevice *device; + + window = gtk_widget_get_parent_window (GTK_WIDGET (textview)); + display = gdk_window_get_display (window); + device_manager = gdk_display_get_device_manager (display); + device = gdk_device_manager_get_client_pointer (device_manager); + + gdk_window_get_device_position (window, device, NULL, NULL, &mt); + } + + update_ctrl_state (textview, (mt & GDK_CONTROL_MASK) != 0); + + if (event->type != GDK_BUTTON_RELEASE) + return FALSE; + + gdk_event_get_button (event, &event_button); + gdk_event_get_coords (event, &event_x_win, &event_y_win); + + if (event_button != 1 || (mt & GDK_CONTROL_MASK) == 0) + return FALSE; + + buffer = gtk_text_view_get_buffer (textview); + + /* we shouldn't follow a link if the user has selected something */ + gtk_text_buffer_get_selection_bounds (buffer, &start, &end); + if (gtk_text_iter_get_offset (&start) != gtk_text_iter_get_offset (&end)) + return FALSE; + + gtk_text_view_window_to_buffer_coords ( + textview, + GTK_TEXT_WINDOW_WIDGET, + event_x_win, event_y_win, &x, &y); + + gtk_text_view_get_iter_at_location (textview, &iter, x, y); + + invoke_link_if_present (buffer, &iter); + update_mouse_cursor (textview, x, y); + + return FALSE; +} + +static gboolean +textview_motion_notify_event (GtkTextView *textview, + GdkEventMotion *event) +{ + gint x, y; + + g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE); + + gtk_text_view_window_to_buffer_coords ( + textview, + GTK_TEXT_WINDOW_WIDGET, + event->x, event->y, &x, &y); + + update_mouse_cursor (textview, x, y); + + return FALSE; +} + +static gboolean +textview_visibility_notify_event (GtkTextView *textview, + GdkEventVisibility *event) +{ + gint wx, wy, bx, by; + + g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE); + + get_pointer_position (textview, &wx, &wy); + + gtk_text_view_window_to_buffer_coords ( + textview, + GTK_TEXT_WINDOW_WIDGET, + wx, wy, &bx, &by); + + update_mouse_cursor (textview, bx, by); + + return FALSE; +} + +void +e_buffer_tagger_connect (GtkTextView *textview) +{ + GtkTextBuffer *buffer; + GtkTextTagTable *tag_table; + GtkTextTag *tag; + + init_magic_links (); + + g_return_if_fail (textview != NULL); + g_return_if_fail (GTK_IS_TEXT_VIEW (textview)); + + buffer = gtk_text_view_get_buffer (textview); + tag_table = gtk_text_buffer_get_tag_table (buffer); + tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG); + + /* if tag is there already, then it is connected, thus claim */ + g_return_if_fail (tag == NULL); + + gtk_text_buffer_create_tag ( + buffer, E_BUFFER_TAGGER_LINK_TAG, + "foreground", "blue", + "underline", PANGO_UNDERLINE_SINGLE, + NULL); + + set_state (buffer, E_BUFFER_TAGGER_STATE_NONE); + + g_signal_connect ( + buffer, "insert-text", + G_CALLBACK (buffer_insert_text), NULL); + g_signal_connect ( + buffer, "delete-range", + G_CALLBACK (buffer_delete_range), NULL); + g_signal_connect ( + buffer, "notify::cursor-position", + G_CALLBACK (buffer_cursor_position), NULL); + + gtk_widget_set_has_tooltip (GTK_WIDGET (textview), TRUE); + + g_signal_connect ( + textview, "query-tooltip", + G_CALLBACK (textview_query_tooltip), NULL); + g_signal_connect ( + textview, "key-press-event", + G_CALLBACK (textview_key_press_event), NULL); + g_signal_connect ( + textview, "event-after", + G_CALLBACK (textview_event_after), NULL); + g_signal_connect ( + textview, "motion-notify-event", + G_CALLBACK (textview_motion_notify_event), NULL); + g_signal_connect ( + textview, "visibility-notify-event", + G_CALLBACK (textview_visibility_notify_event), NULL); +} + +void +e_buffer_tagger_disconnect (GtkTextView *textview) +{ + GtkTextBuffer *buffer; + GtkTextTagTable *tag_table; + GtkTextTag *tag; + + g_return_if_fail (textview != NULL); + g_return_if_fail (GTK_IS_TEXT_VIEW (textview)); + + buffer = gtk_text_view_get_buffer (textview); + tag_table = gtk_text_buffer_get_tag_table (buffer); + tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG); + + /* if tag is not there, then it is not connected, thus claim */ + g_return_if_fail (tag != NULL); + + gtk_text_tag_table_remove (tag_table, tag); + + set_state (buffer, E_BUFFER_TAGGER_STATE_NONE); + + g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_insert_text), NULL); + g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_delete_range), NULL); + g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_cursor_position), NULL); + + gtk_widget_set_has_tooltip (GTK_WIDGET (textview), FALSE); + + g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_query_tooltip), NULL); + g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_key_press_event), NULL); + g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_event_after), NULL); + g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_motion_notify_event), NULL); + g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_visibility_notify_event), NULL); +} + +void +e_buffer_tagger_update_tags (GtkTextView *textview) +{ + GtkTextBuffer *buffer; + GtkTextTagTable *tag_table; + GtkTextTag *tag; + + g_return_if_fail (textview != NULL); + g_return_if_fail (GTK_IS_TEXT_VIEW (textview)); + + buffer = gtk_text_view_get_buffer (textview); + tag_table = gtk_text_buffer_get_tag_table (buffer); + tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG); + + /* if tag is not there, then it is not connected, thus claim */ + g_return_if_fail (tag != NULL); + + update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL | E_BUFFER_TAGGER_STATE_CHANGED, FALSE); + + markup_text (buffer); +} diff --git a/e-util/e-buffer-tagger.h b/e-util/e-buffer-tagger.h new file mode 100644 index 0000000000..f00606ea44 --- /dev/null +++ b/e-util/e-buffer-tagger.h @@ -0,0 +1,39 @@ +/* + * e-buffer-tagger.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_BUFFER_TAGGER_H +#define E_BUFFER_TAGGER_H + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +void e_buffer_tagger_connect (GtkTextView *textview); +void e_buffer_tagger_disconnect (GtkTextView *textview); +void e_buffer_tagger_update_tags (GtkTextView *textview); + +G_END_DECLS + +#endif /* E_BUFFER_TAGGER_H */ diff --git a/e-util/e-cal-source-config.c b/e-util/e-cal-source-config.c new file mode 100644 index 0000000000..e009ac6650 --- /dev/null +++ b/e-util/e-cal-source-config.c @@ -0,0 +1,431 @@ +/* + * e-cal-source-config.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-cal-source-config.h" + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "e-misc-utils.h" + +#define E_CAL_SOURCE_CONFIG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CAL_SOURCE_CONFIG, ECalSourceConfigPrivate)) + +struct _ECalSourceConfigPrivate { + ECalClientSourceType source_type; + GtkWidget *color_button; + GtkWidget *default_button; +}; + +enum { + PROP_0, + PROP_SOURCE_TYPE +}; + +G_DEFINE_TYPE ( + ECalSourceConfig, + e_cal_source_config, + E_TYPE_SOURCE_CONFIG) + +static ESource * +cal_source_config_ref_default (ESourceConfig *config) +{ + ECalSourceConfigPrivate *priv; + ESourceRegistry *registry; + + priv = E_CAL_SOURCE_CONFIG_GET_PRIVATE (config); + registry = e_source_config_get_registry (config); + + if (priv->source_type == E_CAL_CLIENT_SOURCE_TYPE_EVENTS) + return e_source_registry_ref_default_calendar (registry); + else if (priv->source_type == E_CAL_CLIENT_SOURCE_TYPE_MEMOS) + return e_source_registry_ref_default_memo_list (registry); + else if (priv->source_type == E_CAL_CLIENT_SOURCE_TYPE_TASKS) + return e_source_registry_ref_default_task_list (registry); + + g_return_val_if_reached (NULL); +} + +static void +cal_source_config_set_default (ESourceConfig *config, + ESource *source) +{ + ECalSourceConfigPrivate *priv; + ESourceRegistry *registry; + + priv = E_CAL_SOURCE_CONFIG_GET_PRIVATE (config); + registry = e_source_config_get_registry (config); + + if (priv->source_type == E_CAL_CLIENT_SOURCE_TYPE_EVENTS) + e_source_registry_set_default_calendar (registry, source); + else if (priv->source_type == E_CAL_CLIENT_SOURCE_TYPE_MEMOS) + e_source_registry_set_default_memo_list (registry, source); + else if (priv->source_type == E_CAL_CLIENT_SOURCE_TYPE_TASKS) + e_source_registry_set_default_task_list (registry, source); +} + +static void +cal_source_config_set_source_type (ECalSourceConfig *config, + ECalClientSourceType source_type) +{ + config->priv->source_type = source_type; +} + +static void +cal_source_config_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SOURCE_TYPE: + cal_source_config_set_source_type ( + E_CAL_SOURCE_CONFIG (object), + g_value_get_enum (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +cal_source_config_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SOURCE_TYPE: + g_value_set_enum ( + value, + e_cal_source_config_get_source_type ( + E_CAL_SOURCE_CONFIG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +cal_source_config_dispose (GObject *object) +{ + ECalSourceConfigPrivate *priv; + + priv = E_CAL_SOURCE_CONFIG_GET_PRIVATE (object); + + if (priv->color_button != NULL) { + g_object_unref (priv->color_button); + priv->color_button = NULL; + } + + if (priv->default_button != NULL) { + g_object_unref (priv->default_button); + priv->default_button = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_cal_source_config_parent_class)->dispose (object); +} + +static void +cal_source_config_constructed (GObject *object) +{ + ECalSourceConfigPrivate *priv; + ESource *default_source; + ESource *original_source; + ESourceConfig *config; + GObjectClass *class; + GtkWidget *widget; + const gchar *label; + + /* Chain up to parent's constructed() method. */ + class = G_OBJECT_CLASS (e_cal_source_config_parent_class); + class->constructed (object); + + config = E_SOURCE_CONFIG (object); + priv = E_CAL_SOURCE_CONFIG_GET_PRIVATE (object); + + widget = gtk_color_button_new (); + priv->color_button = g_object_ref_sink (widget); + gtk_widget_show (widget); + + switch (priv->source_type) { + case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: + label = _("Mark as default calendar"); + break; + case E_CAL_CLIENT_SOURCE_TYPE_TASKS: + label = _("Mark as default task list"); + break; + case E_CAL_CLIENT_SOURCE_TYPE_MEMOS: + label = _("Mark as default memo list"); + break; + default: + /* No need to translate this string. */ + label = "Invalid ECalSourceType value"; + g_warn_if_reached (); + } + + widget = gtk_check_button_new_with_label (label); + priv->default_button = g_object_ref_sink (widget); + gtk_widget_show (widget); + + default_source = cal_source_config_ref_default (config); + original_source = e_source_config_get_original_source (config); + + if (original_source != NULL) { + gboolean active; + + active = e_source_equal (original_source, default_source); + g_object_set (priv->default_button, "active", active, NULL); + } + + g_object_unref (default_source); + + e_source_config_insert_widget ( + config, NULL, _("Color:"), priv->color_button); + + e_source_config_insert_widget ( + config, NULL, NULL, priv->default_button); +} + +static const gchar * +cal_source_config_get_backend_extension_name (ESourceConfig *config) +{ + ECalSourceConfig *cal_config; + const gchar *extension_name; + + cal_config = E_CAL_SOURCE_CONFIG (config); + + switch (e_cal_source_config_get_source_type (cal_config)) { + case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: + extension_name = E_SOURCE_EXTENSION_CALENDAR; + break; + case E_CAL_CLIENT_SOURCE_TYPE_TASKS: + extension_name = E_SOURCE_EXTENSION_TASK_LIST; + break; + case E_CAL_CLIENT_SOURCE_TYPE_MEMOS: + extension_name = E_SOURCE_EXTENSION_MEMO_LIST; + break; + default: + g_return_val_if_reached (NULL); + } + + return extension_name; +} + +static GList * +cal_source_config_list_eligible_collections (ESourceConfig *config) +{ + GQueue trash = G_QUEUE_INIT; + GList *list, *link; + + /* Chain up to parent's list_eligible_collections() method. */ + list = E_SOURCE_CONFIG_CLASS (e_cal_source_config_parent_class)-> + list_eligible_collections (config); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESource *source = E_SOURCE (link->data); + ESourceCollection *extension; + const gchar *extension_name; + + extension_name = E_SOURCE_EXTENSION_COLLECTION; + extension = e_source_get_extension (source, extension_name); + + if (!e_source_collection_get_calendar_enabled (extension)) + g_queue_push_tail (&trash, link); + } + + /* Remove ineligible collections from the list. */ + while ((link = g_queue_pop_head (&trash)) != NULL) { + g_object_unref (link->data); + list = g_list_delete_link (list, link); + } + + return list; +} + +static void +cal_source_config_init_candidate (ESourceConfig *config, + ESource *scratch_source) +{ + ECalSourceConfigPrivate *priv; + ESourceConfigClass *class; + ESourceExtension *extension; + const gchar *extension_name; + + /* Chain up to parent's init_candidate() method. */ + class = E_SOURCE_CONFIG_CLASS (e_cal_source_config_parent_class); + class->init_candidate (config, scratch_source); + + priv = E_CAL_SOURCE_CONFIG_GET_PRIVATE (config); + + extension_name = e_source_config_get_backend_extension_name (config); + extension = e_source_get_extension (scratch_source, extension_name); + + g_object_bind_property_full ( + extension, "color", + priv->color_button, "color", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE, + e_binding_transform_string_to_color, + e_binding_transform_color_to_string, + NULL, (GDestroyNotify) NULL); +} + +static void +cal_source_config_commit_changes (ESourceConfig *config, + ESource *scratch_source) +{ + ECalSourceConfigPrivate *priv; + GtkToggleButton *toggle_button; + ESourceConfigClass *class; + ESource *default_source; + + priv = E_CAL_SOURCE_CONFIG_GET_PRIVATE (config); + toggle_button = GTK_TOGGLE_BUTTON (priv->default_button); + + /* Chain up to parent's commit_changes() method. */ + class = E_SOURCE_CONFIG_CLASS (e_cal_source_config_parent_class); + class->commit_changes (config, scratch_source); + + default_source = cal_source_config_ref_default (config); + + /* The default setting is a little tricky to get right. If + * the toggle button is active, this ESource is now the default. + * That much is simple. But if the toggle button is NOT active, + * then we have to inspect the old default. If this ESource WAS + * the default, reset the default to 'system'. If this ESource + * WAS NOT the old default, leave it alone. */ + if (gtk_toggle_button_get_active (toggle_button)) + cal_source_config_set_default (config, scratch_source); + else if (e_source_equal (scratch_source, default_source)) + cal_source_config_set_default (config, NULL); + + g_object_unref (default_source); +} + +static void +e_cal_source_config_class_init (ECalSourceConfigClass *class) +{ + GObjectClass *object_class; + ESourceConfigClass *source_config_class; + + g_type_class_add_private (class, sizeof (ECalSourceConfigPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = cal_source_config_set_property; + object_class->get_property = cal_source_config_get_property; + object_class->dispose = cal_source_config_dispose; + object_class->constructed = cal_source_config_constructed; + + source_config_class = E_SOURCE_CONFIG_CLASS (class); + source_config_class->get_backend_extension_name = + cal_source_config_get_backend_extension_name; + source_config_class->list_eligible_collections = + cal_source_config_list_eligible_collections; + source_config_class->init_candidate = cal_source_config_init_candidate; + source_config_class->commit_changes = cal_source_config_commit_changes; + + g_object_class_install_property ( + object_class, + PROP_SOURCE_TYPE, + g_param_spec_enum ( + "source-type", + "Source Type", + "The iCalendar object type", + E_TYPE_CAL_CLIENT_SOURCE_TYPE, + E_CAL_CLIENT_SOURCE_TYPE_EVENTS, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_cal_source_config_init (ECalSourceConfig *config) +{ + config->priv = E_CAL_SOURCE_CONFIG_GET_PRIVATE (config); +} + +GtkWidget * +e_cal_source_config_new (ESourceRegistry *registry, + ESource *original_source, + ECalClientSourceType source_type) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + if (original_source != NULL) + g_return_val_if_fail (E_IS_SOURCE (original_source), NULL); + + return g_object_new ( + E_TYPE_CAL_SOURCE_CONFIG, "registry", registry, + "original-source", original_source, "source-type", + source_type, NULL); +} + +ECalClientSourceType +e_cal_source_config_get_source_type (ECalSourceConfig *config) +{ + g_return_val_if_fail (E_IS_CAL_SOURCE_CONFIG (config), 0); + + return config->priv->source_type; +} + +void +e_cal_source_config_add_offline_toggle (ECalSourceConfig *config, + ESource *scratch_source) +{ + GtkWidget *widget; + ESourceExtension *extension; + const gchar *extension_name; + const gchar *label; + + g_return_if_fail (E_IS_CAL_SOURCE_CONFIG (config)); + g_return_if_fail (E_IS_SOURCE (scratch_source)); + + extension_name = E_SOURCE_EXTENSION_OFFLINE; + extension = e_source_get_extension (scratch_source, extension_name); + + switch (e_cal_source_config_get_source_type (config)) { + case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: + label = _("Copy calendar contents locally " + "for offline operation"); + break; + case E_CAL_CLIENT_SOURCE_TYPE_TASKS: + label = _("Copy task list contents locally " + "for offline operation"); + break; + case E_CAL_CLIENT_SOURCE_TYPE_MEMOS: + label = _("Copy memo list contents locally " + "for offline operation"); + break; + default: + g_return_if_reached (); + } + + widget = gtk_check_button_new_with_label (label); + e_source_config_insert_widget ( + E_SOURCE_CONFIG (config), scratch_source, NULL, widget); + gtk_widget_show (widget); + + g_object_bind_property ( + extension, "stay-synchronized", + widget, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); +} diff --git a/e-util/e-cal-source-config.h b/e-util/e-cal-source-config.h new file mode 100644 index 0000000000..dc78f70e87 --- /dev/null +++ b/e-util/e-cal-source-config.h @@ -0,0 +1,76 @@ +/* + * e-cal-source-config.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CAL_SOURCE_CONFIG_H +#define E_CAL_SOURCE_CONFIG_H + +#include <libecal/libecal.h> +#include <e-util/e-source-config.h> + +/* Standard GObject macros */ +#define E_TYPE_CAL_SOURCE_CONFIG \ + (e_cal_source_config_get_type ()) +#define E_CAL_SOURCE_CONFIG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CAL_SOURCE_CONFIG, ECalSourceConfig)) +#define E_CAL_SOURCE_CONFIG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CAL_SOURCE_CONFIG, ECalSourceConfigClass)) +#define E_IS_CAL_SOURCE_CONFIG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CAL_SOURCE_CONFIG)) +#define E_IS_CAL_SOURCE_CONFIG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CAL_SOURCE_CONFIG)) +#define E_CAL_SOURCE_CONFIG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CAL_SOURCE_CONFIG, ECalSourceConfigClass)) + +G_BEGIN_DECLS + +typedef struct _ECalSourceConfig ECalSourceConfig; +typedef struct _ECalSourceConfigClass ECalSourceConfigClass; +typedef struct _ECalSourceConfigPrivate ECalSourceConfigPrivate; + +struct _ECalSourceConfig { + ESourceConfig parent; + ECalSourceConfigPrivate *priv; +}; + +struct _ECalSourceConfigClass { + ESourceConfigClass parent_class; +}; + +GType e_cal_source_config_get_type (void) G_GNUC_CONST; +GtkWidget * e_cal_source_config_new (ESourceRegistry *registry, + ESource *original_source, + ECalClientSourceType source_type); +ECalClientSourceType + e_cal_source_config_get_source_type + (ECalSourceConfig *config); +void e_cal_source_config_add_offline_toggle + (ECalSourceConfig *config, + ESource *scratch_source); + +G_END_DECLS + +#endif /* E_CAL_SOURCE_CONFIG_H */ diff --git a/e-util/e-calendar-item.c b/e-util/e-calendar-item.c new file mode 100644 index 0000000000..3e7715414c --- /dev/null +++ b/e-util/e-calendar-item.c @@ -0,0 +1,3773 @@ +/* + * ECalendarItem - canvas item displaying a calendar. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <libebackend/libebackend.h> + +#include "e-calendar-item.h" + +#include <time.h> +#include <stdlib.h> +#include <string.h> +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n.h> + +#include "ea-widgets.h" +#include "e-misc-utils.h" + +static const gint e_calendar_item_days_in_month[12] = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 +}; + +#define DAYS_IN_MONTH(year, month) \ + e_calendar_item_days_in_month[month] + (((month) == 1 \ + && ((year) % 4 == 0 && ((year) % 100 != 0 || (year) % 400 == 0))) ? 1 : 0) + +static void e_calendar_item_dispose (GObject *object); +static void e_calendar_item_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); +static void e_calendar_item_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); +static void e_calendar_item_realize (GnomeCanvasItem *item); +static void e_calendar_item_unmap (GnomeCanvasItem *item); +static void e_calendar_item_update (GnomeCanvasItem *item, + const cairo_matrix_t *i2c, + gint flags); +static void e_calendar_item_draw (GnomeCanvasItem *item, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height); +static void e_calendar_item_draw_month (ECalendarItem *calitem, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height, + gint row, + gint col); +static void e_calendar_item_draw_day_numbers + (ECalendarItem *calitem, + cairo_t *cr, + gint width, + gint height, + gint row, + gint col, + gint year, + gint month, + gint start_weekday, + gint cells_x, + gint cells_y); +static GnomeCanvasItem *e_calendar_item_point (GnomeCanvasItem *item, + gdouble x, + gdouble y, + gint cx, + gint cy); +static void e_calendar_item_stop_selecting (ECalendarItem *calitem, + guint32 time); +static void e_calendar_item_selection_add_days + (ECalendarItem *calitem, + gint n_days, + gboolean multi_selection); +static gint e_calendar_item_key_press_event (ECalendarItem *item, + GdkEvent *event); +static gint e_calendar_item_event (GnomeCanvasItem *item, + GdkEvent *event); +static void e_calendar_item_bounds (GnomeCanvasItem *item, + gdouble *x1, + gdouble *y1, + gdouble *x2, + gdouble *y2); + +static gboolean e_calendar_item_button_press (ECalendarItem *calitem, + GdkEvent *event); +static gboolean e_calendar_item_button_release (ECalendarItem *calitem, + GdkEvent *event); +static gboolean e_calendar_item_motion (ECalendarItem *calitem, + GdkEvent *event); + +static gboolean e_calendar_item_convert_position_to_day + (ECalendarItem *calitem, + gint x, + gint y, + gboolean round_empty_positions, + gint *month_offset, + gint *day, + gboolean *entire_week); +static void e_calendar_item_get_month_info (ECalendarItem *calitem, + gint row, + gint col, + gint *first_day_offset, + gint *days_in_month, + gint *days_in_prev_month); +static void e_calendar_item_recalc_sizes (ECalendarItem *calitem); + +static void e_calendar_item_get_day_style (ECalendarItem *calitem, + gint year, + gint month, + gint day, + gint day_style, + gboolean today, + gboolean prev_or_next_month, + gboolean selected, + gboolean has_focus, + gboolean drop_target, + GdkColor **bg_color, + GdkColor **fg_color, + GdkColor **box_color, + gboolean *bold, + gboolean *italic); +static void e_calendar_item_check_selection_end + (ECalendarItem *calitem, + gint start_month, + gint start_day, + gint *end_month, + gint *end_day); +static void e_calendar_item_check_selection_start + (ECalendarItem *calitem, + gint *start_month, + gint *start_day, + gint end_month, + gint end_day); +static void e_calendar_item_add_days_to_selection + (ECalendarItem *calitem, + gint days); +static void e_calendar_item_round_up_selection + (ECalendarItem *calitem, + gint *month_offset, + gint *day); +static void e_calendar_item_round_down_selection + (ECalendarItem *calitem, + gint *month_offset, + gint *day); +static gint e_calendar_item_get_inclusive_days + (ECalendarItem *calitem, + gint start_month_offset, + gint start_day, + gint end_month_offset, + gint end_day); +static void e_calendar_item_ensure_valid_day + (ECalendarItem *calitem, + gint *month_offset, + gint *day); +static gboolean e_calendar_item_ensure_days_visible + (ECalendarItem *calitem, + gint start_year, + gint start_month, + gint start_day, + gint end_year, + gint end_month, + gint end_day, + gboolean emission); +static void e_calendar_item_show_popup_menu (ECalendarItem *calitem, + GdkEvent *button_event, + gint month_offset); +static void e_calendar_item_on_menu_item_activate + (GtkWidget *menuitem, + ECalendarItem *calitem); +static void e_calendar_item_position_menu (GtkMenu *menu, + gint *x, + gint *y, + gboolean *push_in, + gpointer user_data); +static void e_calendar_item_date_range_changed + (ECalendarItem *calitem); +static void e_calendar_item_queue_signal_emission + (ECalendarItem *calitem); +static gboolean e_calendar_item_signal_emission_idle_cb + (gpointer data); +static void e_calendar_item_set_selection_if_emission + (ECalendarItem *calitem, + const GDate *start_date, + const GDate *end_date, + gboolean emission); + +/* Our arguments. */ +enum { + PROP_0, + PROP_YEAR, + PROP_MONTH, + PROP_X1, + PROP_Y1, + PROP_X2, + PROP_Y2, + PROP_FONT_DESC, + PROP_WEEK_NUMBER_FONT, + PROP_WEEK_NUMBER_FONT_DESC, + PROP_ROW_HEIGHT, + PROP_COLUMN_WIDTH, + PROP_MINIMUM_ROWS, + PROP_MINIMUM_COLUMNS, + PROP_MAXIMUM_ROWS, + PROP_MAXIMUM_COLUMNS, + PROP_WEEK_START_DAY, + PROP_SHOW_WEEK_NUMBERS, + PROP_KEEP_WDAYS_ON_WEEKNUM_CLICK, + PROP_MAXIMUM_DAYS_SELECTED, + PROP_DAYS_TO_START_WEEK_SELECTION, + PROP_MOVE_SELECTION_WHEN_MOVING, + PROP_PRESERVE_DAY_WHEN_MOVING, + PROP_DISPLAY_POPUP +}; + +enum { + DATE_RANGE_CHANGED, + SELECTION_CHANGED, + SELECTION_PREVIEW_CHANGED, + LAST_SIGNAL +}; + +static guint e_calendar_item_signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE_WITH_CODE ( + ECalendarItem, + e_calendar_item, + GNOME_TYPE_CANVAS_ITEM, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +static void +e_calendar_item_class_init (ECalendarItemClass *class) +{ + GObjectClass *object_class; + GnomeCanvasItemClass *item_class; + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = e_calendar_item_dispose; + object_class->get_property = e_calendar_item_get_property; + object_class->set_property = e_calendar_item_set_property; + + item_class = GNOME_CANVAS_ITEM_CLASS (class); + item_class->realize = e_calendar_item_realize; + item_class->unmap = e_calendar_item_unmap; + item_class->update = e_calendar_item_update; + item_class->draw = e_calendar_item_draw; + item_class->point = e_calendar_item_point; + item_class->event = e_calendar_item_event; + item_class->bounds = e_calendar_item_bounds; + + class->date_range_changed = NULL; + class->selection_changed = NULL; + class->selection_preview_changed = NULL; + + g_object_class_install_property ( + object_class, + PROP_YEAR, + g_param_spec_int ( + "year", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MONTH, + g_param_spec_int ( + "month", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_X1, + g_param_spec_double ( + "x1", + NULL, + NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0., + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_Y1, + g_param_spec_double ( + "y1", + NULL, + NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0., + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_X2, + g_param_spec_double ( + "x2", + NULL, + NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0., + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_Y2, + g_param_spec_double ( + "y2", + NULL, + NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0., + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_FONT_DESC, + g_param_spec_boxed ( + "font_desc", + NULL, + NULL, + PANGO_TYPE_FONT_DESCRIPTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WEEK_NUMBER_FONT_DESC, + g_param_spec_boxed ( + "week_number_font_desc", + NULL, + NULL, + PANGO_TYPE_FONT_DESCRIPTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_ROW_HEIGHT, + g_param_spec_int ( + "row_height", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_COLUMN_WIDTH, + g_param_spec_int ( + "column_width", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_MINIMUM_ROWS, + g_param_spec_int ( + "minimum_rows", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MINIMUM_COLUMNS, + g_param_spec_int ( + "minimum_columns", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MAXIMUM_ROWS, + g_param_spec_int ( + "maximum_rows", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MAXIMUM_COLUMNS, + g_param_spec_int ( + "maximum_columns", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WEEK_START_DAY, + g_param_spec_int ( + "week_start_day", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_WEEK_NUMBERS, + g_param_spec_boolean ( + "show_week_numbers", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_KEEP_WDAYS_ON_WEEKNUM_CLICK, + g_param_spec_boolean ( + "keep_wdays_on_weeknum_click", + NULL, + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MAXIMUM_DAYS_SELECTED, + g_param_spec_int ( + "maximum_days_selected", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_DAYS_TO_START_WEEK_SELECTION, + g_param_spec_int ( + "days_to_start_week_selection", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MOVE_SELECTION_WHEN_MOVING, + g_param_spec_boolean ( + "move_selection_when_moving", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_PRESERVE_DAY_WHEN_MOVING, + g_param_spec_boolean ( + "preserve_day_when_moving", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_DISPLAY_POPUP, + g_param_spec_boolean ( + "display_popup", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + e_calendar_item_signals[DATE_RANGE_CHANGED] = g_signal_new ( + "date_range_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (ECalendarItemClass, date_range_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + e_calendar_item_signals[SELECTION_CHANGED] = g_signal_new ( + "selection_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (ECalendarItemClass, selection_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + e_calendar_item_signals[SELECTION_PREVIEW_CHANGED] = g_signal_new ( + "selection_preview_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ECalendarItemClass, selection_preview_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + e_calendar_item_a11y_init (); +} + +static void +e_calendar_item_init (ECalendarItem *calitem) +{ + struct tm *tmp_tm; + time_t t; + + /* Set the default time to the current month. */ + t = time (NULL); + tmp_tm = localtime (&t); + calitem->year = tmp_tm->tm_year + 1900; + calitem->month = tmp_tm->tm_mon; + + calitem->styles = NULL; + + calitem->min_cols = 1; + calitem->min_rows = 1; + calitem->max_cols = -1; + calitem->max_rows = -1; + + calitem->rows = 0; + calitem->cols = 0; + + calitem->show_week_numbers = FALSE; + calitem->keep_wdays_on_weeknum_click = FALSE; + calitem->week_start_day = 0; + calitem->expand = TRUE; + calitem->max_days_selected = 1; + calitem->days_to_start_week_selection = -1; + calitem->move_selection_when_moving = TRUE; + calitem->preserve_day_when_moving = FALSE; + calitem->display_popup = TRUE; + + calitem->x1 = 0.0; + calitem->y1 = 0.0; + calitem->x2 = 0.0; + calitem->y2 = 0.0; + + calitem->selecting = FALSE; + calitem->selecting_axis = NULL; + + calitem->selection_set = FALSE; + + calitem->selection_changed = FALSE; + calitem->date_range_changed = FALSE; + + calitem->style_callback = NULL; + calitem->style_callback_data = NULL; + calitem->style_callback_destroy = NULL; + + calitem->time_callback = NULL; + calitem->time_callback_data = NULL; + calitem->time_callback_destroy = NULL; + + calitem->signal_emission_idle_id = 0; +} + +static void +e_calendar_item_dispose (GObject *object) +{ + ECalendarItem *calitem; + + calitem = E_CALENDAR_ITEM (object); + + e_calendar_item_set_style_callback (calitem, NULL, NULL, NULL); + e_calendar_item_set_get_time_callback (calitem, NULL, NULL, NULL); + + if (calitem->styles) { + g_free (calitem->styles); + calitem->styles = NULL; + } + + if (calitem->signal_emission_idle_id > 0) { + g_source_remove (calitem->signal_emission_idle_id); + calitem->signal_emission_idle_id = -1; + } + + if (calitem->font_desc) { + pango_font_description_free (calitem->font_desc); + calitem->font_desc = NULL; + } + + if (calitem->week_number_font_desc) { + pango_font_description_free (calitem->week_number_font_desc); + calitem->week_number_font_desc = NULL; + } + + if (calitem->selecting_axis) + g_free (calitem->selecting_axis); + + G_OBJECT_CLASS (e_calendar_item_parent_class)->dispose (object); +} + +static void +e_calendar_item_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ECalendarItem *calitem; + + calitem = E_CALENDAR_ITEM (object); + + switch (property_id) { + case PROP_YEAR: + g_value_set_int (value, calitem->year); + return; + case PROP_MONTH: + g_value_set_int (value, calitem->month); + return; + case PROP_X1: + g_value_set_double (value, calitem->x1); + return; + case PROP_Y1: + g_value_set_double (value, calitem->y1); + return; + case PROP_X2: + g_value_set_double (value, calitem->x2); + return; + case PROP_Y2: + g_value_set_double (value, calitem->y2); + return; + case PROP_FONT_DESC: + g_value_set_boxed (value, calitem->font_desc); + return; + case PROP_WEEK_NUMBER_FONT_DESC: + g_value_set_boxed (value, calitem->week_number_font_desc); + return; + case PROP_ROW_HEIGHT: + e_calendar_item_recalc_sizes (calitem); + g_value_set_int (value, calitem->min_month_height); + return; + case PROP_COLUMN_WIDTH: + e_calendar_item_recalc_sizes (calitem); + g_value_set_int (value, calitem->min_month_width); + return; + case PROP_MINIMUM_ROWS: + g_value_set_int (value, calitem->min_rows); + return; + case PROP_MINIMUM_COLUMNS: + g_value_set_int (value, calitem->min_cols); + return; + case PROP_MAXIMUM_ROWS: + g_value_set_int (value, calitem->max_rows); + return; + case PROP_MAXIMUM_COLUMNS: + g_value_set_int (value, calitem->max_cols); + return; + case PROP_WEEK_START_DAY: + g_value_set_int (value, calitem->week_start_day); + return; + case PROP_SHOW_WEEK_NUMBERS: + g_value_set_boolean (value, calitem->show_week_numbers); + return; + case PROP_KEEP_WDAYS_ON_WEEKNUM_CLICK: + g_value_set_boolean (value, calitem->keep_wdays_on_weeknum_click); + return; + case PROP_MAXIMUM_DAYS_SELECTED: + g_value_set_int (value, e_calendar_item_get_max_days_sel (calitem)); + return; + case PROP_DAYS_TO_START_WEEK_SELECTION: + g_value_set_int (value, e_calendar_item_get_days_start_week_sel (calitem)); + return; + case PROP_MOVE_SELECTION_WHEN_MOVING: + g_value_set_boolean (value, calitem->move_selection_when_moving); + return; + case PROP_PRESERVE_DAY_WHEN_MOVING: + g_value_set_boolean (value, calitem->preserve_day_when_moving); + return; + case PROP_DISPLAY_POPUP: + g_value_set_boolean (value, e_calendar_item_get_display_popup (calitem)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +e_calendar_item_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GnomeCanvasItem *item; + ECalendarItem *calitem; + PangoFontDescription *font_desc; + gdouble dvalue; + gint ivalue; + gboolean bvalue; + + item = GNOME_CANVAS_ITEM (object); + calitem = E_CALENDAR_ITEM (object); + + switch (property_id) { + case PROP_YEAR: + ivalue = g_value_get_int (value); + e_calendar_item_set_first_month ( + calitem, ivalue, calitem->month); + return; + case PROP_MONTH: + ivalue = g_value_get_int (value); + e_calendar_item_set_first_month ( + calitem, calitem->year, ivalue); + return; + case PROP_X1: + dvalue = g_value_get_double (value); + if (calitem->x1 != dvalue) { + calitem->x1 = dvalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_Y1: + dvalue = g_value_get_double (value); + if (calitem->y1 != dvalue) { + calitem->y1 = dvalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_X2: + dvalue = g_value_get_double (value); + if (calitem->x2 != dvalue) { + calitem->x2 = dvalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_Y2: + dvalue = g_value_get_double (value); + if (calitem->y2 != dvalue) { + calitem->y2 = dvalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_FONT_DESC: + font_desc = g_value_get_boxed (value); + if (calitem->font_desc) + pango_font_description_free (calitem->font_desc); + calitem->font_desc = pango_font_description_copy (font_desc); + gnome_canvas_item_request_update (item); + return; + case PROP_WEEK_NUMBER_FONT_DESC: + font_desc = g_value_get_boxed (value); + if (calitem->week_number_font_desc) + pango_font_description_free (calitem->week_number_font_desc); + calitem->week_number_font_desc = pango_font_description_copy (font_desc); + gnome_canvas_item_request_update (item); + return; + case PROP_MINIMUM_ROWS: + ivalue = g_value_get_int (value); + ivalue = MAX (1, ivalue); + if (calitem->min_rows != ivalue) { + calitem->min_rows = ivalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_MINIMUM_COLUMNS: + ivalue = g_value_get_int (value); + ivalue = MAX (1, ivalue); + if (calitem->min_cols != ivalue) { + calitem->min_cols = ivalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_MAXIMUM_ROWS: + ivalue = g_value_get_int (value); + if (calitem->max_rows != ivalue) { + calitem->max_rows = ivalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_MAXIMUM_COLUMNS: + ivalue = g_value_get_int (value); + if (calitem->max_cols != ivalue) { + calitem->max_cols = ivalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_WEEK_START_DAY: + ivalue = g_value_get_int (value); + if (calitem->week_start_day != ivalue) { + calitem->week_start_day = ivalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_SHOW_WEEK_NUMBERS: + bvalue = g_value_get_boolean (value); + if (calitem->show_week_numbers != bvalue) { + calitem->show_week_numbers = bvalue; + gnome_canvas_item_request_update (item); + } + return; + case PROP_KEEP_WDAYS_ON_WEEKNUM_CLICK: + calitem->keep_wdays_on_weeknum_click = g_value_get_boolean (value); + return; + case PROP_MAXIMUM_DAYS_SELECTED: + ivalue = g_value_get_int (value); + e_calendar_item_set_max_days_sel (calitem, ivalue); + return; + case PROP_DAYS_TO_START_WEEK_SELECTION: + ivalue = g_value_get_int (value); + e_calendar_item_set_days_start_week_sel (calitem, ivalue); + return; + case PROP_MOVE_SELECTION_WHEN_MOVING: + bvalue = g_value_get_boolean (value); + calitem->move_selection_when_moving = bvalue; + return; + case PROP_PRESERVE_DAY_WHEN_MOVING: + bvalue = g_value_get_boolean (value); + calitem->preserve_day_when_moving = bvalue; + return; + case PROP_DISPLAY_POPUP: + bvalue = g_value_get_boolean (value); + e_calendar_item_set_display_popup (calitem, bvalue); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +e_calendar_item_realize (GnomeCanvasItem *item) +{ + ECalendarItem *calitem; + + if (GNOME_CANVAS_ITEM_CLASS (e_calendar_item_parent_class)->realize) + (* GNOME_CANVAS_ITEM_CLASS (e_calendar_item_parent_class)->realize) (item); + + calitem = E_CALENDAR_ITEM (item); + + e_calendar_item_style_set (GTK_WIDGET (item->canvas), calitem); + + e_extensible_load_extensions (E_EXTENSIBLE (calitem)); +} + +static void +e_calendar_item_unmap (GnomeCanvasItem *item) +{ + ECalendarItem *calitem; + + calitem = E_CALENDAR_ITEM (item); + + if (calitem->selecting) { + gnome_canvas_item_ungrab (item, GDK_CURRENT_TIME); + calitem->selecting = FALSE; + } + + if (GNOME_CANVAS_ITEM_CLASS (e_calendar_item_parent_class)->unmap) + (* GNOME_CANVAS_ITEM_CLASS (e_calendar_item_parent_class)->unmap) (item); +} + +static void +e_calendar_item_update (GnomeCanvasItem *item, + const cairo_matrix_t *i2c, + gint flags) +{ + GnomeCanvasItemClass *item_class; + ECalendarItem *calitem; + GtkStyle *style; + gint char_height, width, height, space, space_per_cal, space_per_cell; + gint rows, cols, xthickness, ythickness; + PangoFontDescription *font_desc; + PangoContext *pango_context; + PangoFontMetrics *font_metrics; + + item_class = GNOME_CANVAS_ITEM_CLASS (e_calendar_item_parent_class); + if (item_class->update != NULL) + item_class->update (item, i2c, flags); + + calitem = E_CALENDAR_ITEM (item); + style = gtk_widget_get_style (GTK_WIDGET (item->canvas)); + xthickness = style->xthickness; + ythickness = style->ythickness; + + item->x1 = calitem->x1; + item->y1 = calitem->y1; + item->x2 = calitem->x2 >= calitem->x1 ? calitem->x2 : calitem->x1; + item->y2 = calitem->y2 >= calitem->y1 ? calitem->y2 : calitem->y1; + + /* Set up Pango prerequisites */ + font_desc = style->font_desc; + pango_context = gtk_widget_get_pango_context (GTK_WIDGET (item->canvas)); + font_metrics = pango_context_get_metrics ( + pango_context, font_desc, + pango_context_get_language (pango_context)); + + /* + * Calculate the new layout of the calendar. + */ + + /* Make sure the minimum row width & cell height and the widths of + * all the digits and characters are up to date. */ + e_calendar_item_recalc_sizes (calitem); + + /* Calculate how many rows & cols we can fit in. */ + width = item->x2 - item->x1; + height = item->y2 - item->y1; + + width -= xthickness * 2; + height -= ythickness * 2; + + if (calitem->min_month_height == 0) + rows = 1; + else + rows = height / calitem->min_month_height; + rows = MAX (rows, calitem->min_rows); + if (calitem->max_rows > 0) + rows = MIN (rows, calitem->max_rows); + + if (calitem->min_month_width == 0) + cols = 1; + else + cols = width / calitem->min_month_width; + cols = MAX (cols, calitem->min_cols); + if (calitem->max_cols > 0) + cols = MIN (cols, calitem->max_cols); + + if (rows != calitem->rows || cols != calitem->cols) + e_calendar_item_date_range_changed (calitem); + + calitem->rows = rows; + calitem->cols = cols; + + /* Split up the empty space according to the configuration. + * If the calendar is set to expand, we divide the space between the + * cells and the spaces around the calendar, otherwise we place the + * calendars in the center of the available area. */ + + char_height = + PANGO_PIXELS (pango_font_metrics_get_ascent (font_metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (font_metrics)); + + calitem->month_width = calitem->min_month_width; + calitem->month_height = calitem->min_month_height; + calitem->cell_width = MAX (calitem->max_day_width, (calitem->max_digit_width * 2)) + + E_CALENDAR_ITEM_MIN_CELL_XPAD; + calitem->cell_height = char_height + + E_CALENDAR_ITEM_MIN_CELL_YPAD; + calitem->month_tpad = 0; + calitem->month_bpad = 0; + calitem->month_lpad = 0; + calitem->month_rpad = 0; + + space = height - calitem->rows * calitem->month_height; + if (space > 0) { + space_per_cal = space / calitem->rows; + calitem->month_height += space_per_cal; + + if (calitem->expand) { + space_per_cell = space_per_cal / E_CALENDAR_ROWS_PER_MONTH; + calitem->cell_height += space_per_cell; + space_per_cal -= space_per_cell * E_CALENDAR_ROWS_PER_MONTH; + } + + calitem->month_tpad = space_per_cal / 2; + calitem->month_bpad = space_per_cal - calitem->month_tpad; + } + + space = width - calitem->cols * calitem->month_width; + if (space > 0) { + space_per_cal = space / calitem->cols; + calitem->month_width += space_per_cal; + space -= space_per_cal * calitem->cols; + + if (calitem->expand) { + space_per_cell = space_per_cal / E_CALENDAR_COLS_PER_MONTH; + calitem->cell_width += space_per_cell; + space_per_cal -= space_per_cell * E_CALENDAR_COLS_PER_MONTH; + } + + calitem->month_lpad = space_per_cal / 2; + calitem->month_rpad = space_per_cal - calitem->month_lpad; + } + + space = MAX (0, space); + calitem->x_offset = space / 2; + + gnome_canvas_request_redraw ( + item->canvas, item->x1, item->y1, + item->x2, item->y2); + + pango_font_metrics_unref (font_metrics); +} + +/* + * DRAWING ROUTINES - functions to paint the canvas item. + */ +static void +e_calendar_item_draw (GnomeCanvasItem *canvas_item, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height) +{ + ECalendarItem *calitem; + GtkWidget *widget; + GtkStyleContext *style_context; + gint char_height, row, col, row_y, bar_height, col_x; + const PangoFontDescription *font_desc; + PangoContext *pango_context; + PangoFontMetrics *font_metrics; + GdkRGBA bg_color; + GtkBorder border; + +#if 0 + g_print ( + "In e_calendar_item_draw %i,%i %ix%i\n", + x, y, width, height); +#endif + calitem = E_CALENDAR_ITEM (canvas_item); + + widget = GTK_WIDGET (canvas_item->canvas); + style_context = gtk_widget_get_style_context (widget); + + /* Set up Pango prerequisites */ + font_desc = calitem->font_desc; + if (!font_desc) + font_desc = gtk_style_context_get_font ( + style_context, GTK_STATE_FLAG_NORMAL); + pango_context = gtk_widget_get_pango_context ( + GTK_WIDGET (canvas_item->canvas)); + font_metrics = pango_context_get_metrics ( + pango_context, font_desc, + pango_context_get_language (pango_context)); + + char_height = + PANGO_PIXELS (pango_font_metrics_get_ascent (font_metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (font_metrics)); + + gtk_style_context_get_background_color ( + style_context, GTK_STATE_NORMAL, &bg_color); + + gtk_style_context_get_border ( + style_context, GTK_STATE_NORMAL, &border); + + /* Clear the entire background. */ + cairo_save (cr); + gdk_cairo_set_source_rgba (cr, &bg_color); + cairo_rectangle ( + cr, calitem->x1 - x, calitem->y1 - y, + calitem->x2 - calitem->x1 + 1, + calitem->y2 - calitem->y1 + 1); + cairo_fill (cr); + cairo_restore (cr); + + /* Draw the shadow around the entire item. */ + gtk_style_context_save (style_context); + gtk_style_context_add_class ( + style_context, GTK_STYLE_CLASS_ENTRY); + cairo_save (cr); + gtk_render_frame ( + style_context, cr, + (gdouble) calitem->x1 - x, + (gdouble) calitem->y1 - y, + (gdouble) calitem->x2 - calitem->x1 + 1, + (gdouble) calitem->y2 - calitem->y1 + 1); + cairo_restore (cr); + gtk_style_context_restore (style_context); + + row_y = canvas_item->y1 + border.top; + bar_height = + border.top + border.bottom + + E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME + char_height + + E_CALENDAR_ITEM_YPAD_BELOW_MONTH_NAME; + + for (row = 0; row < calitem->rows; row++) { + /* Draw the background for the title bars and the shadow around + * it, and the vertical lines between columns. */ + + cairo_save (cr); + gdk_cairo_set_source_rgba (cr, &bg_color); + cairo_rectangle ( + cr, calitem->x1 + border.left - x, + row_y - y, + calitem->x2 - calitem->x1 + 1 - + (border.left + border.right), + bar_height); + cairo_fill (cr); + cairo_restore (cr); + + gtk_style_context_save (style_context); + gtk_style_context_add_class ( + style_context, GTK_STYLE_CLASS_HEADER); + cairo_save (cr); + gtk_render_frame ( + style_context, cr, + (gdouble) calitem->x1 + border.left - x, + (gdouble) row_y - y, + (gdouble) calitem->x2 - calitem->x1 + 1 - + (border.left + border.right), + (gdouble) bar_height); + cairo_restore (cr); + gtk_style_context_restore (style_context); + + for (col = 0; col < calitem->cols; col++) { + if (col != 0) { + col_x = calitem->x1 + calitem->x_offset + + calitem->month_width * col; + + gtk_style_context_save (style_context); + gtk_style_context_add_class ( + style_context, + GTK_STYLE_CLASS_SEPARATOR); + cairo_save (cr); + gtk_render_line ( + style_context, cr, + (gdouble) col_x - 1 - x, + (gdouble) row_y + border.top + 1 - y, + (gdouble) row_y + bar_height - + border.bottom - 2 - y, + (gdouble) col_x - x); + cairo_restore (cr); + gtk_style_context_restore (style_context); + } + + e_calendar_item_draw_month ( + calitem, cr, x, y, + width, height, row, col); + } + + row_y += calitem->month_height; + } + + pango_font_metrics_unref (font_metrics); +} + +static void +layout_set_day_text (ECalendarItem *calitem, + PangoLayout *layout, + gint day_index) +{ + const gchar *abbr_name; + + /* day_index: 0 = Monday ... 6 = Sunday */ + abbr_name = e_get_weekday_name (day_index + 1, TRUE); + pango_layout_set_text (layout, abbr_name, -1); +} + +static void +e_calendar_item_draw_month (ECalendarItem *calitem, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height, + gint row, + gint col) +{ + GnomeCanvasItem *item; + GtkWidget *widget; + GtkStyle *style; + PangoFontDescription *font_desc; + struct tm tmp_tm; + GdkRectangle clip_rect; + gint char_height, xthickness, ythickness, start_weekday; + gint year, month; + gint month_x, month_y, month_w, month_h; + gint min_x, max_x, text_x, text_y; + gint day, day_index, cells_x, cells_y, min_cell_width, text_width, arrow_button_size; + gint clip_width, clip_height; + gchar buffer[64]; + PangoContext *pango_context; + PangoFontMetrics *font_metrics; + PangoLayout *layout; + +#if 0 + g_print ( + "In e_calendar_item_draw_month: %i,%i %ix%i row:%i col:%i\n", + x, y, width, height, row, col); +#endif + item = GNOME_CANVAS_ITEM (calitem); + widget = GTK_WIDGET (item->canvas); + style = gtk_widget_get_style (widget); + + /* Set up Pango prerequisites */ + font_desc = calitem->font_desc; + if (!font_desc) + font_desc = style->font_desc; + pango_context = gtk_widget_get_pango_context (widget); + font_metrics = pango_context_get_metrics ( + pango_context, font_desc, + pango_context_get_language (pango_context)); + + char_height = + PANGO_PIXELS (pango_font_metrics_get_ascent (font_metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (font_metrics)); + xthickness = style->xthickness; + ythickness = style->ythickness; + arrow_button_size = + PANGO_PIXELS (pango_font_metrics_get_ascent (font_metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (font_metrics)) + + E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME + + E_CALENDAR_ITEM_YPAD_BELOW_MONTH_NAME + + 2 * xthickness; + + pango_font_metrics_unref (font_metrics); + + /* Calculate the top-left position of the entire month display. */ + month_x = item->x1 + xthickness + calitem->x_offset + + ((gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) + ? (calitem->cols - 1 - col) : col) * calitem->month_width - x; + month_w = item->x2 - item->x1 - xthickness * 2; + month_w = MIN (month_w, calitem->month_width); + month_y = item->y1 + ythickness + row * calitem->month_height - y; + month_h = item->y2 - item->y1 - ythickness * 2; + month_h = MIN (month_h, calitem->month_height); + + /* Just return if the month is outside the given area. */ + if (month_x >= width || month_x + calitem->month_width <= 0 + || month_y >= height || month_y + calitem->month_height <= 0) + return; + + month = calitem->month + row * calitem->cols + col; + year = calitem->year + month / 12; + month %= 12; + + /* Draw the month name & year, with clipping. Note that the top row + * needs extra space around it for the buttons. */ + + layout = gtk_widget_create_pango_layout (widget, NULL); + + if (row == 0 && col == 0) + min_x = E_CALENDAR_ITEM_XPAD_BEFORE_MONTH_NAME_WITH_BUTTON; + else + min_x = E_CALENDAR_ITEM_XPAD_BEFORE_MONTH_NAME; + + max_x = month_w; + if (row == 0 && col == 0) + max_x -= E_CALENDAR_ITEM_XPAD_AFTER_MONTH_NAME_WITH_BUTTON; + else + max_x -= E_CALENDAR_ITEM_XPAD_AFTER_MONTH_NAME; + + text_y = month_y + style->ythickness + + E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME; + clip_rect.x = month_x + min_x; + clip_rect.x = MAX (0, clip_rect.x); + clip_rect.y = MAX (0, text_y); + + memset (&tmp_tm, 0, sizeof (tmp_tm)); + tmp_tm.tm_year = year - 1900; + tmp_tm.tm_mon = month; + tmp_tm.tm_mday = 1; + tmp_tm.tm_isdst = -1; + mktime (&tmp_tm); + start_weekday = (tmp_tm.tm_wday + 6) % 7; + + if (month_x + max_x - clip_rect.x > 0) { + cairo_save (cr); + + clip_rect.width = month_x + max_x - clip_rect.x; + clip_rect.height = text_y + char_height - clip_rect.y; + gdk_cairo_rectangle (cr, &clip_rect); + cairo_clip (cr); + + gdk_cairo_set_source_color (cr, &style->fg[GTK_STATE_NORMAL]); + + if (row == 0 && col == 0) { + PangoLayout *layout_yr; + gchar buffer_yr[64]; + gdouble max_width; + + layout_yr = gtk_widget_create_pango_layout (widget, NULL); + + /* This is a strftime() format. %B = Month name. */ + e_utf8_strftime (buffer, sizeof (buffer), C_("CalItem", "%B"), &tmp_tm); + /* This is a strftime() format. %Y = Year. */ + e_utf8_strftime (buffer_yr, sizeof (buffer_yr), C_("CalItem", "%Y"), &tmp_tm); + + pango_layout_set_font_description (layout, font_desc); + pango_layout_set_text (layout, buffer, -1); + + pango_layout_set_font_description (layout_yr, font_desc); + pango_layout_set_text (layout_yr, buffer_yr, -1); + + if (gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL) { + max_width = calitem->max_month_name_width; + pango_layout_get_pixel_size (layout, &text_width, NULL); + + cairo_move_to (cr, month_x + min_x + arrow_button_size + (max_width - text_width) / 2, text_y); + pango_cairo_show_layout (cr, layout); + + max_width = calitem->max_digit_width * 5; + pango_layout_get_pixel_size (layout_yr, &text_width, NULL); + + cairo_move_to (cr, month_x + month_w - arrow_button_size - (max_width - text_width) / 2 - text_width - min_x, text_y); + pango_cairo_show_layout (cr, layout_yr); + } else { + max_width = calitem->max_digit_width * 5; + pango_layout_get_pixel_size (layout_yr, &text_width, NULL); + + cairo_move_to (cr, month_x + min_x + arrow_button_size + (max_width - text_width) / 2, text_y); + pango_cairo_show_layout (cr, layout_yr); + + max_width = calitem->max_month_name_width; + pango_layout_get_pixel_size (layout, &text_width, NULL); + + cairo_move_to (cr, month_x + month_w - arrow_button_size - (max_width - text_width) / 2 - text_width - min_x, text_y); + pango_cairo_show_layout (cr, layout); + } + + g_object_unref (layout_yr); + } else { + /* This is a strftime() format. %B = Month name, %Y = Year. */ + e_utf8_strftime (buffer, sizeof (buffer), C_("CalItem", "%B %Y"), &tmp_tm); + + pango_layout_set_font_description (layout, font_desc); + pango_layout_set_text (layout, buffer, -1); + + /* Ideally we place the text centered in the month, but we + * won't go to the left of the minimum x position. */ + pango_layout_get_pixel_size (layout, &text_width, NULL); + text_x = (calitem->month_width - text_width) / 2; + text_x = MAX (min_x, text_x); + + cairo_move_to (cr, month_x + text_x, text_y); + pango_cairo_show_layout (cr, layout); + } + + cairo_restore (cr); + } + + /* Set the clip rectangle for the main month display. */ + clip_rect.x = MAX (0, month_x); + clip_rect.y = MAX (0, month_y); + clip_width = month_x + month_w - clip_rect.x; + clip_height = month_y + month_h - clip_rect.y; + + if (clip_width <= 0 || clip_height <= 0) { + g_object_unref (layout); + return; + } + + clip_rect.width = clip_width; + clip_rect.height = clip_height; + + cairo_save (cr); + + gdk_cairo_rectangle (cr, &clip_rect); + cairo_clip (cr); + + /* Draw the day initials across the top of the month. */ + min_cell_width = MAX (calitem->max_day_width, (calitem->max_digit_width * 2)) + + E_CALENDAR_ITEM_MIN_CELL_XPAD; + + cells_x = month_x + E_CALENDAR_ITEM_XPAD_BEFORE_WEEK_NUMBERS + calitem->month_lpad + + E_CALENDAR_ITEM_XPAD_BEFORE_CELLS; + if (calitem->show_week_numbers) + cells_x += calitem->max_week_number_digit_width * 2 + + E_CALENDAR_ITEM_XPAD_AFTER_WEEK_NUMBERS + 1; + text_x = cells_x + calitem->cell_width + - (calitem->cell_width - min_cell_width) / 2; + text_x -= E_CALENDAR_ITEM_MIN_CELL_XPAD / 2; + text_y = month_y + ythickness * 2 + + E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME + + char_height + E_CALENDAR_ITEM_YPAD_BELOW_MONTH_NAME + + E_CALENDAR_ITEM_YPAD_ABOVE_DAY_LETTERS + calitem->month_tpad; + + cells_y = text_y + char_height + + E_CALENDAR_ITEM_YPAD_BELOW_DAY_LETTERS + 1 + + E_CALENDAR_ITEM_YPAD_ABOVE_CELLS; + + cairo_save (cr); + gdk_cairo_set_source_color (cr, &style->base[GTK_STATE_SELECTED]); + cairo_rectangle ( + cr, cells_x , + text_y - E_CALENDAR_ITEM_YPAD_ABOVE_CELLS - 1, + calitem->cell_width * 7 , cells_y - text_y); + cairo_fill (cr); + cairo_restore (cr); + + day_index = calitem->week_start_day; + pango_layout_set_font_description (layout, font_desc); + if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) + text_x += (7 - 1) * calitem->cell_width; + gdk_cairo_set_source_color (cr, &style->text[GTK_STATE_ACTIVE]); + for (day = 0; day < 7; day++) { + cairo_save (cr); + layout_set_day_text (calitem, layout, day_index); + cairo_move_to ( + cr, + text_x - calitem->day_widths[day_index], + text_y); + pango_cairo_show_layout (cr, layout); + text_x += (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) + ? -calitem->cell_width : calitem->cell_width; + day_index++; + if (day_index == 7) + day_index = 0; + cairo_restore (cr); + } + + /* Draw the rectangle around the week number. */ + if (calitem->show_week_numbers) { + cairo_save (cr); + gdk_cairo_set_source_color (cr, &style->base[GTK_STATE_SELECTED]); + cairo_rectangle ( + cr, cells_x, cells_y - (cells_y - text_y + 2) , + -20, E_CALENDAR_ROWS_PER_MONTH * calitem->cell_height + 18); + cairo_fill (cr); + cairo_restore (cr); + } + + e_calendar_item_draw_day_numbers ( + calitem, cr, width, height, row, col, + year, month, start_weekday, cells_x, cells_y); + + g_object_unref (layout); + cairo_restore (cr); +} + +static const gchar * +get_digit_fomat (void) +{ + +#ifdef HAVE_GNU_GET_LIBC_VERSION +#include <gnu/libc-version.h> + + const gchar *libc_version = gnu_get_libc_version (); + gchar **split = g_strsplit (libc_version, ".", -1); + gint major = 0; + gint minor = 0; + gint revision = 0; + + major = atoi (split[0]); + minor = atoi (split[1]); + + if (g_strv_length (split) > 2) + revision = atoi (split[2]); + g_strfreev (split); + + if (major > 2 || minor > 2 || (minor == 2 && revision > 2)) { + return "%Id"; + } +#endif + + return "%d"; +} + +static void +e_calendar_item_draw_day_numbers (ECalendarItem *calitem, + cairo_t *cr, + gint width, + gint height, + gint row, + gint col, + gint year, + gint month, + gint start_weekday, + gint cells_x, + gint cells_y) +{ + GnomeCanvasItem *item; + GtkWidget *widget; + GtkStyle *style; + PangoFontDescription *font_desc; + GdkColor *bg_color, *fg_color, *box_color; + struct tm today_tm; + time_t t; + gint char_height, min_cell_width, min_cell_height; + gint day_num, drow, dcol, day_x, day_y; + gint text_x, text_y; + gint num_chars, digit; + gint week_num, mon, days_from_week_start; + gint years[3], months[3], days_in_month[3]; + gboolean today, selected, has_focus, drop_target = FALSE; + gboolean bold, italic, draw_day, finished = FALSE; + gint today_year, today_month, today_mday, month_offset; + gchar buffer[9]; + gint day_style = 0; + PangoContext *pango_context; + PangoFontMetrics *font_metrics; + PangoLayout *layout; + + item = GNOME_CANVAS_ITEM (calitem); + widget = GTK_WIDGET (item->canvas); + style = gtk_widget_get_style (widget); + + /* Set up Pango prerequisites */ + font_desc = calitem->font_desc; + if (!font_desc) + font_desc = style->font_desc; + + pango_context = gtk_widget_get_pango_context (widget); + font_metrics = pango_context_get_metrics ( + pango_context, font_desc, + pango_context_get_language (pango_context)); + + char_height = + PANGO_PIXELS (pango_font_metrics_get_ascent (font_metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (font_metrics)); + + min_cell_width = MAX (calitem->max_day_width, (calitem->max_digit_width * 2)) + + E_CALENDAR_ITEM_MIN_CELL_XPAD; + min_cell_height = char_height + E_CALENDAR_ITEM_MIN_CELL_YPAD; + + layout = pango_cairo_create_layout (cr); + + /* Calculate the number of days in the previous, current, and next + * months. */ + years[0] = years[1] = years[2] = year; + months[0] = month - 1; + months[1] = month; + months[2] = month + 1; + if (months[0] == -1) { + months[0] = 11; + years[0]--; + } + if (months[2] == 12) { + months[2] = 0; + years[2]++; + } + + days_in_month[0] = DAYS_IN_MONTH (years[0], months[0]); + days_in_month[1] = DAYS_IN_MONTH (years[1], months[1]); + days_in_month[2] = DAYS_IN_MONTH (years[2], months[2]); + + /* Mon 0 is the previous month, which we may show the end of. Mon 1 is + * the current month, and mon 2 is the next month. */ + mon = 0; + + month_offset = row * calitem->cols + col - 1; + day_num = days_in_month[0]; + days_from_week_start = (start_weekday + 7 - calitem->week_start_day) + % 7; + /* For the top-left month we show the end of the previous month, and + * if the new month starts on the first day of the week we show a + * complete week from the previous month. */ + if (days_from_week_start == 0) { + if (row == 0 && col == 0) { + day_num -= 6; + } else { + mon++; + month_offset++; + day_num = 1; + } + } else { + day_num -= days_from_week_start - 1; + } + + /* Get today's date, so we can highlight it. */ + if (calitem->time_callback) { + today_tm = calitem->time_callback ( + calitem, calitem->time_callback_data); + } else { + t = time (NULL); + today_tm = *localtime (&t); + } + today_year = today_tm.tm_year + 1900; + today_month = today_tm.tm_mon; + today_mday = today_tm.tm_mday; + + /* We usually skip the last days of the previous month (mon = 0), + * except for the top-left month displayed. */ + draw_day = (mon == 1 || (row == 0 && col == 0)); + + for (drow = 0; drow < 6; drow++) { + /* Draw the week number. */ + if (calitem->show_week_numbers) { + week_num = e_calendar_item_get_week_number ( + calitem, day_num, months[mon], years[mon]); + + text_x = cells_x - E_CALENDAR_ITEM_XPAD_BEFORE_CELLS - 1 + - E_CALENDAR_ITEM_XPAD_AFTER_WEEK_NUMBERS; + text_y = cells_y + drow * calitem->cell_height + + + (calitem->cell_height - min_cell_height + 1) / 2; + + num_chars = 0; + if (week_num >= 10) { + digit = week_num / 10; + text_x -= calitem->week_number_digit_widths[digit]; + num_chars += sprintf ( + &buffer[num_chars], + get_digit_fomat (), digit); + } + + digit = week_num % 10; + text_x -= calitem->week_number_digit_widths[digit] + 6; + num_chars += sprintf ( + &buffer[num_chars], + get_digit_fomat (), digit); + + cairo_save (cr); + gdk_cairo_set_source_color ( + cr, &style->text[GTK_STATE_ACTIVE]); + pango_layout_set_font_description (layout, font_desc); + pango_layout_set_text (layout, buffer, num_chars); + cairo_move_to (cr, text_x, text_y); + pango_cairo_update_layout (cr, layout); + pango_cairo_show_layout (cr, layout); + cairo_restore (cr); + } + + for (dcol = 0; dcol < 7; dcol++) { + if (draw_day) { + day_x = cells_x + + ((gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) + ? 7 - 1 - dcol : dcol) * calitem->cell_width; + + day_y = cells_y + drow * calitem->cell_height; + + today = years[mon] == today_year + && months[mon] == today_month + && day_num == today_mday; + + selected = calitem->selection_set + && (calitem->selection_start_month_offset < month_offset + || (calitem->selection_start_month_offset == month_offset + && calitem->selection_start_day <= day_num)) + && (calitem->selection_end_month_offset > month_offset + || (calitem->selection_end_month_offset == month_offset + && calitem->selection_end_day >= day_num)); + + if (calitem->styles) + day_style = calitem->styles[(month_offset + 1) * 32 + day_num]; + + /* Get the colors & style to use for the day.*/ + if ((gtk_widget_has_focus (GTK_WIDGET (item->canvas))) && + item->canvas->focused_item == item) + has_focus = TRUE; + else + has_focus = FALSE; + + bold = FALSE; + italic = FALSE; + + if (calitem->style_callback) + calitem->style_callback ( + calitem, + years[mon], + months[mon], + day_num, + day_style, + today, + mon != 1, + selected, + has_focus, + drop_target, + &bg_color, + &fg_color, + &box_color, + &bold, + &italic, + calitem->style_callback_data); + else + e_calendar_item_get_day_style ( + calitem, + years[mon], + months[mon], + day_num, + day_style, + today, + mon != 1, + selected, + has_focus, + drop_target, + &bg_color, + &fg_color, + &box_color, + &bold, + &italic); + + /* Draw the background, if set. */ + if (bg_color) { + cairo_save (cr); + gdk_cairo_set_source_color (cr, bg_color); + cairo_rectangle ( + cr, day_x , day_y, + calitem->cell_width, + calitem->cell_height); + cairo_fill (cr); + cairo_restore (cr); + } + + /* Draw the box, if set. */ + if (box_color) { + cairo_save (cr); + gdk_cairo_set_source_color (cr, box_color); + cairo_rectangle ( + cr, day_x , day_y, + calitem->cell_width - 1, + calitem->cell_height - 1); + cairo_stroke (cr); + cairo_restore (cr); + } + + /* Draw the 1- or 2-digit day number. */ + day_x += calitem->cell_width - + (calitem->cell_width - + min_cell_width) / 2; + day_x -= E_CALENDAR_ITEM_MIN_CELL_XPAD / 2; + day_y += (calitem->cell_height - min_cell_height + 1) / 2; + day_y += E_CALENDAR_ITEM_MIN_CELL_YPAD / 2; + + num_chars = 0; + if (day_num >= 10) { + digit = day_num / 10; + day_x -= calitem->digit_widths[digit]; + num_chars += sprintf ( + &buffer[num_chars], + get_digit_fomat (), digit); + } + + digit = day_num % 10; + day_x -= calitem->digit_widths[digit]; + num_chars += sprintf ( + &buffer[num_chars], + get_digit_fomat (), digit); + + cairo_save (cr); + if (fg_color) { + gdk_cairo_set_source_color ( + cr, fg_color); + } else { + gdk_cairo_set_source_color ( + cr, &style->fg[GTK_STATE_NORMAL]); + } + + if (bold) { + pango_font_description_set_weight ( + font_desc, PANGO_WEIGHT_BOLD); + } else { + pango_font_description_set_weight ( + font_desc, PANGO_WEIGHT_NORMAL); + } + + if (italic) { + pango_font_description_set_style ( + font_desc, PANGO_STYLE_ITALIC); + } else { + pango_font_description_set_style ( + font_desc, PANGO_STYLE_NORMAL); + } + + pango_layout_set_font_description (layout, font_desc); + pango_layout_set_text (layout, buffer, num_chars); + cairo_move_to (cr, day_x, day_y); + pango_cairo_update_layout (cr, layout); + pango_cairo_show_layout (cr, layout); + cairo_restore (cr); + } + + /* See if we've reached the end of a month. */ + if (day_num == days_in_month[mon]) { + month_offset++; + mon++; + /* We only draw the start of the next month + * for the bottom-right month displayed. */ + if (mon == 2 && (row != calitem->rows - 1 + || col != calitem->cols - 1)) { + /* Set a flag so we exit the loop. */ + finished = TRUE; + break; + } + day_num = 1; + draw_day = TRUE; + } else { + day_num++; + } + } + + /* Exit the loop if the flag is set. */ + if (finished) + break; + } + + /* Reset pango weight and style */ + pango_font_description_set_weight (font_desc, PANGO_WEIGHT_NORMAL); + pango_font_description_set_style (font_desc, PANGO_STYLE_NORMAL); + + g_object_unref (layout); + + pango_font_metrics_unref (font_metrics); +} + +gint +e_calendar_item_get_week_number (ECalendarItem *calitem, + gint day, + gint month, + gint year) +{ + GDate date; + guint weekday, yearday; + gint week_num; + + g_date_clear (&date, 1); + g_date_set_dmy (&date, day, month + 1, year); + + /* This results in a value of 0 (Monday) - 6 (Sunday). + * (or -1 on error - oops!!) */ + weekday = g_date_get_weekday (&date) - 1; + + if (weekday > 0) { + /* we want always point to nearest Monday, as the first day of the week, + * regardless of the calendar's week_start_day */ + if (weekday >= 3) + g_date_add_days (&date, 7 - weekday); + else + g_date_subtract_days (&date, weekday); + } + + /* Calculate the day of the year, from 0 to 365. */ + yearday = g_date_get_day_of_year (&date) - 1; + + /* If the week starts on or after 29th December, it is week 1 of the + * next year, since there are 4 days in the next year. */ + if (g_date_get_month (&date) == 12 && g_date_get_day (&date) >= 29) + return 1; + + /* Calculate the week number, from 0. */ + week_num = yearday / 7; + + /* If the first week starts on or after Jan 5th, then we need to add + * 1 since the previous week will really be the first week. */ + if (yearday % 7 >= 4) + week_num++; + + /* Add 1 so week numbers are from 1 to 53. */ + return week_num + 1; +} + +/* This is supposed to return the nearest item the the point and the distance. + * Since we are the only item we just return ourself and 0 for the distance. + * This is needed so that we get button/motion events. */ +static GnomeCanvasItem * +e_calendar_item_point (GnomeCanvasItem *item, + gdouble x, + gdouble y, + gint cx, + gint cy) +{ + return item; +} + +static void +e_calendar_item_stop_selecting (ECalendarItem *calitem, + guint32 time) +{ + if (!calitem->selecting) + return; + + gnome_canvas_item_ungrab (GNOME_CANVAS_ITEM (calitem), time); + + calitem->selecting = FALSE; + + /* If the user selects the grayed dates before the first month or + * after the last month, we move backwards or forwards one month. + * The set_month () call should take care of updating the selection. */ + if (calitem->selection_end_month_offset == -1) + e_calendar_item_set_first_month ( + calitem, calitem->year, + calitem->month - 1); + else if (calitem->selection_start_month_offset == calitem->rows * calitem->cols) + e_calendar_item_set_first_month ( + calitem, calitem->year, + calitem->month + 1); + + calitem->selection_changed = TRUE; + if (calitem->selecting_axis) { + g_free (calitem->selecting_axis); + calitem->selecting_axis = NULL; + } + + e_calendar_item_queue_signal_emission (calitem); + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (calitem)); +} + +static void +e_calendar_item_selection_add_days (ECalendarItem *calitem, + gint n_days, + gboolean multi_selection) +{ + GDate gdate_start, gdate_end; + + g_return_if_fail (E_IS_CALENDAR_ITEM (calitem)); + + if (!e_calendar_item_get_selection (calitem, &gdate_start, &gdate_end)) { + /* We set the date to the first day of the month */ + g_date_set_dmy (&gdate_start, 1, calitem->month + 1, calitem->year); + gdate_end = gdate_start; + } + + if (multi_selection && calitem->max_days_selected > 1) { + gint days_between; + + days_between = g_date_days_between (&gdate_start, &gdate_end); + if (!calitem->selecting_axis) { + calitem->selecting_axis = g_new (GDate, 1); + *(calitem->selecting_axis) = gdate_start; + } + if ((days_between != 0 && + g_date_compare (calitem->selecting_axis, &gdate_end) == 0) || + (days_between == 0 && n_days < 0)) { + if (days_between - n_days > calitem->max_days_selected - 1) + n_days = days_between + 1 - calitem->max_days_selected; + g_date_add_days (&gdate_start, n_days); + } + else { + if (days_between + n_days > calitem->max_days_selected - 1) + n_days = calitem->max_days_selected - 1 - days_between; + g_date_add_days (&gdate_end, n_days); + } + + if (g_date_compare (&gdate_end, &gdate_start) < 0) { + GDate tmp_date; + tmp_date = gdate_start; + gdate_start = gdate_end; + gdate_end = tmp_date; + } + } + else { + /* clear "selecting_axis", it is only for mulit-selecting */ + if (calitem->selecting_axis) { + g_free (calitem->selecting_axis); + calitem->selecting_axis = NULL; + } + g_date_add_days (&gdate_start, n_days); + gdate_end = gdate_start; + } + + calitem->selecting = TRUE; + + e_calendar_item_set_selection_if_emission ( + calitem, &gdate_start, &gdate_end, FALSE); + + g_signal_emit_by_name (calitem, "selection_preview_changed"); +} + +static gint +e_calendar_item_key_press_event (ECalendarItem *calitem, + GdkEvent *event) +{ + guint keyval = event->key.keyval; + gboolean multi_selection = FALSE; + + if (event->key.state & GDK_CONTROL_MASK || + event->key.state & GDK_MOD1_MASK) + return FALSE; + + multi_selection = event->key.state & GDK_SHIFT_MASK; + switch (keyval) { + case GDK_KEY_Up: + e_calendar_item_selection_add_days ( + calitem, -7, + multi_selection); + break; + case GDK_KEY_Down: + e_calendar_item_selection_add_days ( + calitem, 7, + multi_selection); + break; + case GDK_KEY_Left: + e_calendar_item_selection_add_days ( + calitem, -1, + multi_selection); + break; + case GDK_KEY_Right: + e_calendar_item_selection_add_days ( + calitem, 1, + multi_selection); + break; + case GDK_KEY_space: + case GDK_KEY_Return: + e_calendar_item_stop_selecting (calitem, event->key.time); + break; + default: + return FALSE; + } + return TRUE; +} + +static gint +e_calendar_item_event (GnomeCanvasItem *item, + GdkEvent *event) +{ + ECalendarItem *calitem; + + calitem = E_CALENDAR_ITEM (item); + + switch (event->type) { + case GDK_BUTTON_PRESS: + return e_calendar_item_button_press (calitem, event); + case GDK_BUTTON_RELEASE: + return e_calendar_item_button_release (calitem, event); + case GDK_MOTION_NOTIFY: + return e_calendar_item_motion (calitem, event); + case GDK_FOCUS_CHANGE: + gnome_canvas_item_request_update (item); + return FALSE; + case GDK_KEY_PRESS: + return e_calendar_item_key_press_event (calitem, event); + default: + break; + } + + return FALSE; +} + +static void +e_calendar_item_bounds (GnomeCanvasItem *item, + gdouble *x1, + gdouble *y1, + gdouble *x2, + gdouble *y2) +{ + ECalendarItem *calitem; + + g_return_if_fail (E_IS_CALENDAR_ITEM (item)); + + calitem = E_CALENDAR_ITEM (item); + *x1 = calitem->x1; + *y1 = calitem->y1; + *x2 = calitem->x2; + *y2 = calitem->y2; +} + +/* This checks if any fonts have changed, and if so it recalculates the + * text sizes and the minimum month size. */ +static void +e_calendar_item_recalc_sizes (ECalendarItem *calitem) +{ + GnomeCanvasItem *canvas_item; + GtkStyle *style; + gint day, max_day_width, digit, max_digit_width, max_week_number_digit_width; + gint char_height, width, min_cell_width, min_cell_height; + gchar buffer[64]; + struct tm tmp_tm; + PangoFontDescription *font_desc, *wkfont_desc; + PangoContext *pango_context; + PangoFontMetrics *font_metrics; + PangoLayout *layout; + + canvas_item = GNOME_CANVAS_ITEM (calitem); + style = gtk_widget_get_style (GTK_WIDGET (canvas_item->canvas)); + + if (!style) + return; + + /* Set up Pango prerequisites */ + font_desc = calitem->font_desc; + wkfont_desc = calitem->week_number_font_desc; + if (!font_desc) + font_desc = style->font_desc; + + pango_context = gtk_widget_create_pango_context ( + GTK_WIDGET (canvas_item->canvas)); + font_metrics = pango_context_get_metrics ( + pango_context, font_desc, + pango_context_get_language (pango_context)); + layout = pango_layout_new (pango_context); + + char_height = + PANGO_PIXELS (pango_font_metrics_get_ascent (font_metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (font_metrics)); + + max_day_width = 0; + for (day = 0; day < 7; day++) { + layout_set_day_text (calitem, layout, day); + pango_layout_get_pixel_size (layout, &width, NULL); + + calitem->day_widths[day] = width; + max_day_width = MAX (max_day_width, width); + } + calitem->max_day_width = max_day_width; + + max_digit_width = 0; + max_week_number_digit_width = 0; + for (digit = 0; digit < 10; digit++) { + gchar locale_digit[5]; + gint locale_digit_len; + + locale_digit_len = sprintf (locale_digit, get_digit_fomat (), digit); + + pango_layout_set_text (layout, locale_digit, locale_digit_len); + pango_layout_get_pixel_size (layout, &width, NULL); + + calitem->digit_widths[digit] = width; + max_digit_width = MAX (max_digit_width, width); + + if (wkfont_desc) { + pango_context_set_font_description (pango_context, wkfont_desc); + pango_layout_context_changed (layout); + + pango_layout_set_text (layout, locale_digit, locale_digit_len); + pango_layout_get_pixel_size (layout, &width, NULL); + + calitem->week_number_digit_widths[digit] = width; + max_week_number_digit_width = MAX (max_week_number_digit_width, width); + + pango_context_set_font_description (pango_context, font_desc); + pango_layout_context_changed (layout); + } else { + calitem->week_number_digit_widths[digit] = width; + max_week_number_digit_width = max_digit_width; + } + } + calitem->max_digit_width = max_digit_width; + calitem->max_week_number_digit_width = max_week_number_digit_width; + + min_cell_width = MAX (calitem->max_day_width, (calitem->max_digit_width * 2)) + + E_CALENDAR_ITEM_MIN_CELL_XPAD; + min_cell_height = char_height + E_CALENDAR_ITEM_MIN_CELL_YPAD; + + calitem->min_month_width = E_CALENDAR_ITEM_XPAD_BEFORE_WEEK_NUMBERS + + E_CALENDAR_ITEM_XPAD_BEFORE_CELLS + min_cell_width * 7 + + E_CALENDAR_ITEM_XPAD_AFTER_CELLS; + if (calitem->show_week_numbers) { + calitem->min_month_width += calitem->max_week_number_digit_width * 2 + + E_CALENDAR_ITEM_XPAD_AFTER_WEEK_NUMBERS + 1; + } + + calitem->min_month_height = style->ythickness * 2 + + E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME + char_height + + E_CALENDAR_ITEM_YPAD_BELOW_MONTH_NAME + 1 + + E_CALENDAR_ITEM_YPAD_ABOVE_DAY_LETTERS + + char_height + E_CALENDAR_ITEM_YPAD_BELOW_DAY_LETTERS + 1 + + E_CALENDAR_ITEM_YPAD_ABOVE_CELLS + min_cell_height * 6 + + E_CALENDAR_ITEM_YPAD_BELOW_CELLS; + + calitem->max_month_name_width = 50; + memset (&tmp_tm, 0, sizeof (tmp_tm)); + tmp_tm.tm_year = 2000 - 100; + tmp_tm.tm_mday = 1; + tmp_tm.tm_isdst = -1; + for (tmp_tm.tm_mon = 0; tmp_tm.tm_mon < 12; tmp_tm.tm_mon++) { + mktime (&tmp_tm); + + e_utf8_strftime (buffer, sizeof (buffer), C_("CalItem", "%B"), &tmp_tm); + + pango_layout_set_text (layout, buffer, -1); + pango_layout_get_pixel_size (layout, &width, NULL); + + if (width > calitem->max_month_name_width) + calitem->max_month_name_width = width; + } + + g_object_unref (layout); + g_object_unref (pango_context); + pango_font_metrics_unref (font_metrics); +} + +static void +e_calendar_item_get_day_style (ECalendarItem *calitem, + gint year, + gint month, + gint day, + gint day_style, + gboolean today, + gboolean prev_or_next_month, + gboolean selected, + gboolean has_focus, + gboolean drop_target, + GdkColor **bg_color, + GdkColor **fg_color, + GdkColor **box_color, + gboolean *bold, + gboolean *italic) +{ + GtkWidget *widget; + GtkStyle *style; + + widget = GTK_WIDGET (GNOME_CANVAS_ITEM (calitem)->canvas); + style = gtk_widget_get_style (widget); + + *bg_color = NULL; + *fg_color = NULL; + *box_color = NULL; + + *bold = (day_style & E_CALENDAR_ITEM_MARK_BOLD) == + E_CALENDAR_ITEM_MARK_BOLD; + *italic = (day_style & E_CALENDAR_ITEM_MARK_ITALIC) == + E_CALENDAR_ITEM_MARK_ITALIC; + + if (today) + *box_color = &calitem->colors[E_CALENDAR_ITEM_COLOR_TODAY_BOX]; + + if (prev_or_next_month) + *fg_color = &style->mid[gtk_widget_get_state (widget)]; + + if (selected) { + if (has_focus) { + *fg_color = &style->text[GTK_STATE_SELECTED]; + *bg_color = &style->base[GTK_STATE_SELECTED]; + } else { + *fg_color = &style->text[GTK_STATE_ACTIVE]; + *bg_color = &style->base[GTK_STATE_ACTIVE]; + + if ((*bg_color)->red == style->base[GTK_STATE_NORMAL].red && + (*bg_color)->green == style->base[GTK_STATE_NORMAL].green && + (*bg_color)->blue == style->base[GTK_STATE_NORMAL].blue) { + *fg_color = &style->text[GTK_STATE_SELECTED]; + *bg_color = &style->base[GTK_STATE_SELECTED]; + } + } + } +} + +static gboolean +e_calendar_item_button_press (ECalendarItem *calitem, + GdkEvent *button_event) +{ + GdkGrabStatus grab_status; + GdkDevice *event_device; + guint event_button = 0; + guint32 event_time; + gdouble event_x_win = 0; + gdouble event_y_win = 0; + gint month_offset, day, add_days = 0; + gboolean all_week, round_up_end = FALSE, round_down_start = FALSE; + + gdk_event_get_button (button_event, &event_button); + gdk_event_get_coords (button_event, &event_x_win, &event_y_win); + event_device = gdk_event_get_device (button_event); + event_time = gdk_event_get_time (button_event); + + if (event_button == 4) + e_calendar_item_set_first_month ( + calitem, calitem->year, + calitem->month - 1); + else if (event_button == 5) + e_calendar_item_set_first_month ( + calitem, calitem->year, + calitem->month + 1); + + if (!e_calendar_item_convert_position_to_day (calitem, + event_x_win, + event_y_win, + TRUE, + &month_offset, &day, + &all_week)) + return FALSE; + + if (event_button == 3 && day == -1 + && e_calendar_item_get_display_popup (calitem)) { + e_calendar_item_show_popup_menu ( + calitem, button_event, month_offset); + return TRUE; + } + + if (event_button != 1 || day == -1) + return FALSE; + + if (calitem->max_days_selected < 1) + return TRUE; + + grab_status = gnome_canvas_item_grab ( + GNOME_CANVAS_ITEM (calitem), + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_RELEASE_MASK, + NULL, + event_device, + event_time); + + if (grab_status != GDK_GRAB_SUCCESS) + return FALSE; + + if (all_week && calitem->keep_wdays_on_weeknum_click) { + gint tmp_start_moff, tmp_start_day; + + tmp_start_moff = calitem->selection_start_month_offset; + tmp_start_day = calitem->selection_start_day; + e_calendar_item_round_down_selection ( + calitem, &tmp_start_moff, &tmp_start_day); + + e_calendar_item_round_down_selection (calitem, &month_offset, &day); + month_offset += calitem->selection_start_month_offset - tmp_start_moff; + day += calitem->selection_start_day - tmp_start_day; + + /* keep same count of days selected */ + add_days = e_calendar_item_get_inclusive_days ( + calitem, + calitem->selection_start_month_offset, + calitem->selection_start_day, + calitem->selection_end_month_offset, + calitem->selection_end_day) - 1; + } + + calitem->selection_set = TRUE; + calitem->selection_start_month_offset = month_offset; + calitem->selection_start_day = day; + calitem->selection_end_month_offset = month_offset; + calitem->selection_end_day = day; + + if (add_days > 0) + e_calendar_item_add_days_to_selection (calitem, add_days); + + calitem->selection_real_start_month_offset = month_offset; + calitem->selection_real_start_day = day; + + calitem->selection_from_full_week = FALSE; + calitem->selecting = TRUE; + calitem->selection_dragging_end = TRUE; + + if (all_week && !calitem->keep_wdays_on_weeknum_click) { + calitem->selection_from_full_week = TRUE; + round_up_end = TRUE; + } + + if (calitem->days_to_start_week_selection == 1) { + round_down_start = TRUE; + round_up_end = TRUE; + } + + /* Don't round up or down if we can't select a week or more, + * or when keeping week days. */ + if (calitem->max_days_selected < 7 || + (all_week && calitem->keep_wdays_on_weeknum_click)) { + round_down_start = FALSE; + round_up_end = FALSE; + } + + if (round_up_end) + e_calendar_item_round_up_selection ( + calitem, &calitem->selection_end_month_offset, + &calitem->selection_end_day); + + if (round_down_start) + e_calendar_item_round_down_selection ( + calitem, &calitem->selection_start_month_offset, + &calitem->selection_start_day); + + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (calitem)); + + return TRUE; +} + +static gboolean +e_calendar_item_button_release (ECalendarItem *calitem, + GdkEvent *button_event) +{ + guint32 event_time; + + event_time = gdk_event_get_time (button_event); + e_calendar_item_stop_selecting (calitem, event_time); + + return FALSE; +} + +static gboolean +e_calendar_item_motion (ECalendarItem *calitem, + GdkEvent *event) +{ + gint start_month, start_day, end_month, end_day, month_offset, day; + gint tmp_month, tmp_day, days_in_selection; + gboolean all_week, round_up_end = FALSE, round_down_start = FALSE; + + if (!calitem->selecting) + return FALSE; + + if (!e_calendar_item_convert_position_to_day (calitem, + event->button.x, + event->button.y, + TRUE, + &month_offset, &day, + &all_week)) + return FALSE; + + if (day == -1) + return FALSE; + + if (calitem->selection_dragging_end) { + start_month = calitem->selection_real_start_month_offset; + start_day = calitem->selection_real_start_day; + end_month = month_offset; + end_day = day; + } else { + start_month = month_offset; + start_day = day; + end_month = calitem->selection_real_start_month_offset; + end_day = calitem->selection_real_start_day; + } + + if (start_month > end_month || (start_month == end_month + && start_day > end_day)) { + tmp_month = start_month; + tmp_day = start_day; + start_month = end_month; + start_day = end_day; + end_month = tmp_month; + end_day = tmp_day; + + calitem->selection_dragging_end = + !calitem->selection_dragging_end; + } + + if (calitem->days_to_start_week_selection > 0) { + days_in_selection = e_calendar_item_get_inclusive_days ( + calitem, start_month, start_day, end_month, end_day); + if (days_in_selection >= calitem->days_to_start_week_selection) { + round_down_start = TRUE; + round_up_end = TRUE; + } + } + + /* If we are over a week number and we are dragging the end of the + * selection, we round up to the end of this week. */ + if (all_week && calitem->selection_dragging_end) + round_up_end = TRUE; + + /* If the selection was started from a week number and we are dragging + * the start of the selection, we need to round up the end to include + * all of the original week selected. */ + if (calitem->selection_from_full_week + && !calitem->selection_dragging_end) + round_up_end = TRUE; + + /* Don't round up or down if we can't select a week or more. */ + if (calitem->max_days_selected < 7) { + round_down_start = FALSE; + round_up_end = FALSE; + } + + if (round_up_end) + e_calendar_item_round_up_selection ( + calitem, &end_month, + &end_day); + if (round_down_start) + e_calendar_item_round_down_selection ( + calitem, &start_month, + &start_day); + + /* Check we don't go over the maximum number of days to select. */ + if (calitem->selection_dragging_end) { + e_calendar_item_check_selection_end ( + calitem, + start_month, + start_day, + &end_month, + &end_day); + } else { + e_calendar_item_check_selection_start ( + calitem, + &start_month, + &start_day, + end_month, + end_day); + } + + if (start_month == calitem->selection_start_month_offset + && start_day == calitem->selection_start_day + && end_month == calitem->selection_end_month_offset + && end_day == calitem->selection_end_day) + return FALSE; + + calitem->selection_start_month_offset = start_month; + calitem->selection_start_day = start_day; + calitem->selection_end_month_offset = end_month; + calitem->selection_end_day = end_day; + + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (calitem)); + + return TRUE; +} + +static void +e_calendar_item_check_selection_end (ECalendarItem *calitem, + gint start_month, + gint start_day, + gint *end_month, + gint *end_day) +{ + gint year, month, max_month, max_day, days_in_month; + + if (calitem->max_days_selected <= 0) + return; + + year = calitem->year; + month = calitem->month + start_month; + e_calendar_item_normalize_date (calitem, &year, &month); + + max_month = start_month; + max_day = start_day + calitem->max_days_selected - 1; + + for (;;) { + days_in_month = DAYS_IN_MONTH (year, month); + if (max_day <= days_in_month) + break; + max_month++; + month++; + if (month == 12) { + year++; + month = 0; + } + max_day -= days_in_month; + } + + if (*end_month > max_month) { + *end_month = max_month; + *end_day = max_day; + } else if (*end_month == max_month && *end_day > max_day) { + *end_day = max_day; + } +} + +static void +e_calendar_item_check_selection_start (ECalendarItem *calitem, + gint *start_month, + gint *start_day, + gint end_month, + gint end_day) +{ + gint year, month, min_month, min_day, days_in_month; + + if (calitem->max_days_selected <= 0) + return; + + year = calitem->year; + month = calitem->month + end_month; + e_calendar_item_normalize_date (calitem, &year, &month); + + min_month = end_month; + min_day = end_day - calitem->max_days_selected + 1; + + while (min_day <= 0) { + min_month--; + month--; + if (month == -1) { + year--; + month = 11; + } + days_in_month = DAYS_IN_MONTH (year, month); + min_day += days_in_month; + } + + if (*start_month < min_month) { + *start_month = min_month; + *start_day = min_day; + } else if (*start_month == min_month && *start_day < min_day) { + *start_day = min_day; + } +} + +/* Converts a position within the item to a month & day. + * The month returned is 0 for the top-left month displayed. + * If the position is over the month heading -1 is returned for the day. + * If the position is over a week number the first day of the week is returned + * and entire_week is set to TRUE. + * It returns FALSE if the position is completely outside all months. */ +static gboolean +e_calendar_item_convert_position_to_day (ECalendarItem *calitem, + gint event_x, + gint event_y, + gboolean round_empty_positions, + gint *month_offset, + gint *day, + gboolean *entire_week) +{ + GnomeCanvasItem *item; + GtkWidget *widget; + GtkStyle *style; + gint xthickness, ythickness, char_height; + gint x, y, row, col, cells_x, cells_y, day_row, day_col; + gint first_day_offset, days_in_month, days_in_prev_month; + gint week_num_x1, week_num_x2; + PangoFontDescription *font_desc; + PangoContext *pango_context; + PangoFontMetrics *font_metrics; + + item = GNOME_CANVAS_ITEM (calitem); + widget = GTK_WIDGET (item->canvas); + style = gtk_widget_get_style (widget); + + font_desc = calitem->font_desc; + if (!font_desc) + font_desc = style->font_desc; + pango_context = gtk_widget_create_pango_context (widget); + font_metrics = pango_context_get_metrics ( + pango_context, font_desc, + pango_context_get_language (pango_context)); + + char_height = + PANGO_PIXELS (pango_font_metrics_get_ascent (font_metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (font_metrics)); + xthickness = style->xthickness; + ythickness = style->ythickness; + + pango_font_metrics_unref (font_metrics); + + *entire_week = FALSE; + + x = event_x - xthickness - calitem->x_offset; + y = event_y - ythickness; + + if (x < 0 || y < 0) + return FALSE; + + row = y / calitem->month_height; + col = x / calitem->month_width; + + if (row >= calitem->rows || col >= calitem->cols) + return FALSE; + if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) + col = calitem->cols - 1 - col; + + *month_offset = row * calitem->cols + col; + + x = x % calitem->month_width; + y = y % calitem->month_height; + + if (y < ythickness * 2 + E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME + + char_height + E_CALENDAR_ITEM_YPAD_BELOW_MONTH_NAME) { + *day = -1; + return TRUE; + } + + cells_y = ythickness * 2 + E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME + + char_height + E_CALENDAR_ITEM_YPAD_BELOW_MONTH_NAME + + E_CALENDAR_ITEM_YPAD_ABOVE_DAY_LETTERS + calitem->month_tpad + + char_height + E_CALENDAR_ITEM_YPAD_BELOW_DAY_LETTERS + 1 + + E_CALENDAR_ITEM_YPAD_ABOVE_CELLS; + y -= cells_y; + if (y < 0) + return FALSE; + day_row = y / calitem->cell_height; + if (day_row >= E_CALENDAR_ROWS_PER_MONTH) + return FALSE; + + week_num_x1 = E_CALENDAR_ITEM_XPAD_BEFORE_WEEK_NUMBERS + calitem->month_lpad; + + if (calitem->show_week_numbers) { + week_num_x2 = week_num_x1 + + calitem->max_week_number_digit_width * 2; + if (x >= week_num_x1 && x < week_num_x2) + *entire_week = TRUE; + cells_x = week_num_x2 + E_CALENDAR_ITEM_XPAD_AFTER_WEEK_NUMBERS + 1; + } else { + cells_x = week_num_x1; + } + + if (*entire_week) { + day_col = 0; + } else { + cells_x += E_CALENDAR_ITEM_XPAD_BEFORE_CELLS; + x -= cells_x; + if (x < 0) + return FALSE; + day_col = x / calitem->cell_width; + if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) + day_col = E_CALENDAR_COLS_PER_MONTH - 1 - day_col; + if (day_col >= E_CALENDAR_COLS_PER_MONTH) + return FALSE; + } + + *day = day_row * E_CALENDAR_COLS_PER_MONTH + day_col; + + e_calendar_item_get_month_info ( + calitem, row, col, &first_day_offset, + &days_in_month, &days_in_prev_month); + if (*day < first_day_offset) { + if (*entire_week || (row == 0 && col == 0)) { + (*month_offset)--; + *day = days_in_prev_month + 1 - first_day_offset + + *day; + return TRUE; + } else if (round_empty_positions) { + *day = first_day_offset; + } else { + return FALSE; + } + } + + *day -= first_day_offset - 1; + + if (*day > days_in_month) { + if (row == calitem->rows - 1 && col == calitem->cols - 1) { + (*month_offset)++; + *day -= days_in_month; + return TRUE; + } else if (round_empty_positions) { + *day = days_in_month; + } else { + return FALSE; + } + } + + return TRUE; +} + +static void +e_calendar_item_get_month_info (ECalendarItem *calitem, + gint row, + gint col, + gint *first_day_offset, + gint *days_in_month, + gint *days_in_prev_month) +{ + gint year, month, start_weekday, first_day_of_month; + struct tm tmp_tm = { 0 }; + + month = calitem->month + row * calitem->cols + col; + year = calitem->year + month / 12; + month = month % 12; + + *days_in_month = DAYS_IN_MONTH (year, month); + if (month == 0) + *days_in_prev_month = DAYS_IN_MONTH (year - 1, 11); + else + *days_in_prev_month = DAYS_IN_MONTH (year, month - 1); + + tmp_tm.tm_year = year - 1900; + tmp_tm.tm_mon = month; + tmp_tm.tm_mday = 1; + tmp_tm.tm_isdst = -1; + mktime (&tmp_tm); + + /* Convert to 0 (Monday) to 6 (Sunday). */ + start_weekday = (tmp_tm.tm_wday + 6) % 7; + + first_day_of_month = (start_weekday + 7 - calitem->week_start_day) % 7; + + if (row == 0 && col == 0 && first_day_of_month == 0) + *first_day_offset = 7; + else + *first_day_offset = first_day_of_month; +} + +void +e_calendar_item_get_first_month (ECalendarItem *calitem, + gint *year, + gint *month) +{ + *year = calitem->year; + *month = calitem->month; +} + +static void +e_calendar_item_preserve_day_selection (ECalendarItem *calitem, + gint selected_day, + gint *month_offset, + gint *day) +{ + gint year, month, weekday, days, days_in_month; + struct tm tmp_tm = { 0 }; + + year = calitem->year; + month = calitem->month + *month_offset; + e_calendar_item_normalize_date (calitem, &year, &month); + + tmp_tm.tm_year = year - 1900; + tmp_tm.tm_mon = month; + tmp_tm.tm_mday = *day; + tmp_tm.tm_isdst = -1; + mktime (&tmp_tm); + + /* Convert to 0 (Monday) to 6 (Sunday). */ + weekday = (tmp_tm.tm_wday + 6) % 7; + + /* Calculate how many days to the start of the row. */ + days = (weekday + 7 - selected_day) % 7; + + *day -= days; + if (*day <= 0) { + month--; + if (month == -1) { + year--; + month = 11; + } + days_in_month = DAYS_IN_MONTH (year, month); + (*month_offset)--; + *day += days_in_month; + } +} + +/* This also handles values of month < 0 or > 11 by updating the year. */ +void +e_calendar_item_set_first_month (ECalendarItem *calitem, + gint year, + gint month) +{ + gint new_year, new_month, months_diff, num_months; + gint old_days_in_selection, new_days_in_selection; + + new_year = year; + new_month = month; + e_calendar_item_normalize_date (calitem, &new_year, &new_month); + + if (calitem->year == new_year && calitem->month == new_month) + return; + + /* Update the selection. */ + num_months = calitem->rows * calitem->cols; + months_diff = (new_year - calitem->year) * 12 + + new_month - calitem->month; + + if (calitem->selection_set) { + if (!calitem->move_selection_when_moving + || (calitem->selection_start_month_offset - months_diff >= 0 + && calitem->selection_end_month_offset - months_diff < num_months)) { + calitem->selection_start_month_offset -= months_diff; + calitem->selection_end_month_offset -= months_diff; + calitem->selection_real_start_month_offset -= months_diff; + + calitem->year = new_year; + calitem->month = new_month; + } else { + gint selected_day; + struct tm tmp_tm = { 0 }; + + old_days_in_selection = e_calendar_item_get_inclusive_days ( + calitem, + calitem->selection_start_month_offset, + calitem->selection_start_day, + calitem->selection_end_month_offset, + calitem->selection_end_day); + + /* Calculate the currently selected day */ + tmp_tm.tm_year = calitem->year - 1900; + tmp_tm.tm_mon = calitem->month + calitem->selection_start_month_offset; + tmp_tm.tm_mday = calitem->selection_start_day; + tmp_tm.tm_isdst = -1; + mktime (&tmp_tm); + + selected_day = (tmp_tm.tm_wday + 6) % 7; + + /* Make sure the selection will be displayed. */ + if (calitem->selection_start_month_offset < 0 + || calitem->selection_start_month_offset >= num_months) { + calitem->selection_end_month_offset -= + calitem->selection_start_month_offset; + calitem->selection_start_month_offset = 0; + } + + /* We want to ensure that the same number of days are + * selected after we have moved the selection. */ + calitem->year = new_year; + calitem->month = new_month; + + e_calendar_item_ensure_valid_day ( + calitem, &calitem->selection_start_month_offset, + &calitem->selection_start_day); + e_calendar_item_ensure_valid_day ( + calitem, &calitem->selection_end_month_offset, + &calitem->selection_end_day); + + if (calitem->preserve_day_when_moving) { + e_calendar_item_preserve_day_selection ( + calitem, selected_day, + &calitem->selection_start_month_offset, + &calitem->selection_start_day); + } + + new_days_in_selection = e_calendar_item_get_inclusive_days ( + calitem, + calitem->selection_start_month_offset, + calitem->selection_start_day, + calitem->selection_end_month_offset, + calitem->selection_end_day); + + if (old_days_in_selection != new_days_in_selection) + e_calendar_item_add_days_to_selection ( + calitem, old_days_in_selection - + new_days_in_selection); + + /* Flag that we need to emit the "selection_changed" + * signal. We don't want to emit it here since setting + * the "year" and "month" args would result in 2 + * signals emitted. */ + calitem->selection_changed = TRUE; + } + } else { + calitem->year = new_year; + calitem->month = new_month; + } + + e_calendar_item_date_range_changed (calitem); + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (calitem)); +} + +/* Get the maximum number of days selectable */ +gint +e_calendar_item_get_max_days_sel (ECalendarItem *calitem) +{ + return calitem->max_days_selected; +} + +/* Set the maximum number of days selectable */ +void +e_calendar_item_set_max_days_sel (ECalendarItem *calitem, + gint days) +{ + calitem->max_days_selected = MAX (0, days); + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (calitem)); +} + +/* Get the maximum number of days before whole weeks are selected */ +gint +e_calendar_item_get_days_start_week_sel (ECalendarItem *calitem) +{ + return calitem->days_to_start_week_selection; +} + +/* Set the maximum number of days before whole weeks are selected */ +void +e_calendar_item_set_days_start_week_sel (ECalendarItem *calitem, + gint days) +{ + calitem->days_to_start_week_selection = days; +} + +gboolean +e_calendar_item_get_display_popup (ECalendarItem *calitem) +{ + return calitem->display_popup; +} + +void +e_calendar_item_set_display_popup (ECalendarItem *calitem, + gboolean display) +{ + calitem->display_popup = display; +} + +/* This will make sure that the given year & month are valid, i.e. if month + * is < 0 or > 11 the year and month will be updated accordingly. */ +void +e_calendar_item_normalize_date (ECalendarItem *calitem, + gint *year, + gint *month) +{ + if (*month >= 0) { + *year += *month / 12; + *month = *month % 12; + } else { + *year += *month / 12 - 1; + *month = *month % 12; + if (*month != 0) + *month += 12; + } +} + +/* Adds or subtracts days from the selection. It is used when we switch months + * and the selection extends past the end of a month but we want to keep the + * number of days selected the same. days should not be more than 30. */ +static void +e_calendar_item_add_days_to_selection (ECalendarItem *calitem, + gint days) +{ + gint year, month, days_in_month; + + year = calitem->year; + month = calitem->month + calitem->selection_end_month_offset; + e_calendar_item_normalize_date (calitem, &year, &month); + + calitem->selection_end_day += days; + if (calitem->selection_end_day <= 0) { + month--; + e_calendar_item_normalize_date (calitem, &year, &month); + calitem->selection_end_month_offset--; + calitem->selection_end_day += DAYS_IN_MONTH (year, month); + } else { + days_in_month = DAYS_IN_MONTH (year, month); + if (calitem->selection_end_day > days_in_month) { + calitem->selection_end_month_offset++; + calitem->selection_end_day -= days_in_month; + } + } +} + +/* Gets the range of dates actually shown. Months are 0 to 11. + * This also includes the last days of the previous month and the first days + * of the following month, which are normally shown in gray. + * It returns FALSE if no dates are currently shown. */ +gboolean +e_calendar_item_get_date_range (ECalendarItem *calitem, + gint *start_year, + gint *start_month, + gint *start_day, + gint *end_year, + gint *end_month, + gint *end_day) +{ + gint first_day_offset, days_in_month, days_in_prev_month; + + if (calitem->rows == 0 || calitem->cols == 0) + return FALSE; + + /* Calculate the first day shown. This will be one of the greyed-out + * days before the first full month begins. */ + e_calendar_item_get_month_info ( + calitem, 0, 0, &first_day_offset, + &days_in_month, &days_in_prev_month); + *start_year = calitem->year; + *start_month = calitem->month - 1; + if (*start_month == -1) { + (*start_year)--; + *start_month = 11; + } + *start_day = days_in_prev_month + 1 - first_day_offset; + + /* Calculate the last day shown. This will be one of the greyed-out + * days after the last full month ends. */ + e_calendar_item_get_month_info ( + calitem, calitem->rows - 1, + calitem->cols - 1, &first_day_offset, + &days_in_month, &days_in_prev_month); + *end_month = calitem->month + calitem->rows * calitem->cols; + *end_year = calitem->year + *end_month / 12; + *end_month %= 12; + *end_day = E_CALENDAR_ROWS_PER_MONTH * E_CALENDAR_COLS_PER_MONTH + - first_day_offset - days_in_month; + + return TRUE; +} + +/* Simple way to mark days so they appear bold. + * A more flexible interface may be added later. */ +void +e_calendar_item_clear_marks (ECalendarItem *calitem) +{ + GnomeCanvasItem *item; + + item = GNOME_CANVAS_ITEM (calitem); + + g_free (calitem->styles); + calitem->styles = NULL; + + gnome_canvas_request_redraw ( + item->canvas, item->x1, item->y1, + item->x2, item->y2); +} + +/* add_day_style - whether bit-or with the actual style or change the style fully */ +void +e_calendar_item_mark_day (ECalendarItem *calitem, + gint year, + gint month, + gint day, + guint8 day_style, + gboolean add_day_style) +{ + gint month_offset; + gint index; + + month_offset = (year - calitem->year) * 12 + month - calitem->month; + if (month_offset < -1 || month_offset > calitem->rows * calitem->cols) + return; + + if (!calitem->styles) + calitem->styles = g_new0 (guint8, (calitem->rows * calitem->cols + 2) * 32); + + index = (month_offset + 1) * 32 + day; + calitem->styles[index] = day_style | + (add_day_style ? calitem->styles[index] : 0); + + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (calitem)); +} + +void +e_calendar_item_mark_days (ECalendarItem *calitem, + gint start_year, + gint start_month, + gint start_day, + gint end_year, + gint end_month, + gint end_day, + guint8 day_style, + gboolean add_day_style) +{ + gint month_offset, end_month_offset, day; + + month_offset = (start_year - calitem->year) * 12 + start_month + - calitem->month; + day = start_day; + if (month_offset > calitem->rows * calitem->cols) + return; + if (month_offset < -1) { + month_offset = -1; + day = 1; + } + + end_month_offset = (end_year - calitem->year) * 12 + end_month + - calitem->month; + if (end_month_offset < -1) + return; + if (end_month_offset > calitem->rows * calitem->cols) { + end_month_offset = calitem->rows * calitem->cols; + end_day = 31; + } + + if (month_offset > end_month_offset) + return; + + if (!calitem->styles) + calitem->styles = g_new0 (guint8, (calitem->rows * calitem->cols + 2) * 32); + + for (;;) { + gint index; + + if (month_offset == end_month_offset && day > end_day) + break; + + if (month_offset < -1 || month_offset > calitem->rows * calitem->cols) + g_warning ("Bad month offset: %i\n", month_offset); + if (day < 1 || day > 31) + g_warning ("Bad day: %i\n", day); + +#if 0 + g_print ("Marking Month:%i Day:%i\n", month_offset, day); +#endif + index = (month_offset + 1) * 32 + day; + calitem->styles[index] = day_style | + (add_day_style ? calitem->styles[index] : 0); + + day++; + if (day == 32) { + month_offset++; + day = 1; + if (month_offset > end_month_offset) + break; + } + } + + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (calitem)); +} + +/* Rounds up the given day to the end of the week. */ +static void +e_calendar_item_round_up_selection (ECalendarItem *calitem, + gint *month_offset, + gint *day) +{ + gint year, month, weekday, days, days_in_month; + struct tm tmp_tm = { 0 }; + + year = calitem->year; + month = calitem->month + *month_offset; + e_calendar_item_normalize_date (calitem, &year, &month); + + tmp_tm.tm_year = year - 1900; + tmp_tm.tm_mon = month; + tmp_tm.tm_mday = *day; + tmp_tm.tm_isdst = -1; + mktime (&tmp_tm); + + /* Convert to 0 (Monday) to 6 (Sunday). */ + weekday = (tmp_tm.tm_wday + 6) % 7; + + /* Calculate how many days to the end of the row. */ + days = (calitem->week_start_day + 6 - weekday) % 7; + + *day += days; + days_in_month = DAYS_IN_MONTH (year, month); + if (*day > days_in_month) { + (*month_offset)++; + *day -= days_in_month; + } +} + +/* Rounds down the given day to the start of the week. */ +static void +e_calendar_item_round_down_selection (ECalendarItem *calitem, + gint *month_offset, + gint *day) +{ + gint year, month, weekday, days, days_in_month; + struct tm tmp_tm = { 0 }; + + year = calitem->year; + month = calitem->month + *month_offset; + e_calendar_item_normalize_date (calitem, &year, &month); + + tmp_tm.tm_year = year - 1900; + tmp_tm.tm_mon = month; + tmp_tm.tm_mday = *day; + tmp_tm.tm_isdst = -1; + mktime (&tmp_tm); + + /* Convert to 0 (Monday) to 6 (Sunday). */ + weekday = (tmp_tm.tm_wday + 6) % 7; + + /* Calculate how many days to the start of the row. */ + days = (weekday + 7 - calitem->week_start_day) % 7; + + *day -= days; + if (*day <= 0) { + month--; + if (month == -1) { + year--; + month = 11; + } + days_in_month = DAYS_IN_MONTH (year, month); + (*month_offset)--; + *day += days_in_month; + } +} + +static gint +e_calendar_item_get_inclusive_days (ECalendarItem *calitem, + gint start_month_offset, + gint start_day, + gint end_month_offset, + gint end_day) +{ + gint start_year, start_month, end_year, end_month, days = 0; + + start_year = calitem->year; + start_month = calitem->month + start_month_offset; + e_calendar_item_normalize_date (calitem, &start_year, &start_month); + + end_year = calitem->year; + end_month = calitem->month + end_month_offset; + e_calendar_item_normalize_date (calitem, &end_year, &end_month); + + while (start_year < end_year || start_month < end_month) { + days += DAYS_IN_MONTH (start_year, start_month); + start_month++; + if (start_month == 12) { + start_year++; + start_month = 0; + } + } + + days += end_day - start_day + 1; + + return days; +} + +/* If the day is off the end of the month it is set to the last day of the + * month. */ +static void +e_calendar_item_ensure_valid_day (ECalendarItem *calitem, + gint *month_offset, + gint *day) +{ + gint year, month, days_in_month; + + year = calitem->year; + month = calitem->month + *month_offset; + e_calendar_item_normalize_date (calitem, &year, &month); + + days_in_month = DAYS_IN_MONTH (year, month); + if (*day > days_in_month) + *day = days_in_month; +} + +gboolean +e_calendar_item_get_selection (ECalendarItem *calitem, + GDate *start_date, + GDate *end_date) +{ + gint start_year, start_month, start_day; + gint end_year, end_month, end_day; + + g_date_clear (start_date, 1); + g_date_clear (end_date, 1); + + if (!calitem->selection_set) + return FALSE; + + start_year = calitem->year; + start_month = calitem->month + calitem->selection_start_month_offset; + e_calendar_item_normalize_date (calitem, &start_year, &start_month); + start_day = calitem->selection_start_day; + + end_year = calitem->year; + end_month = calitem->month + calitem->selection_end_month_offset; + e_calendar_item_normalize_date (calitem, &end_year, &end_month); + end_day = calitem->selection_end_day; + + g_date_set_dmy (start_date, start_day, start_month + 1, start_year); + g_date_set_dmy (end_date, end_day, end_month + 1, end_year); + + return TRUE; +} + +static void +e_calendar_item_set_selection_if_emission (ECalendarItem *calitem, + const GDate *start_date, + const GDate *end_date, + gboolean emission) +{ + gint start_year, start_month, start_day; + gint end_year, end_month, end_day; + gint new_start_month_offset, new_start_day; + gint new_end_month_offset, new_end_day; + gboolean need_update; + + g_return_if_fail (E_IS_CALENDAR_ITEM (calitem)); + + /* If start_date is NULL, we clear the selection without changing the + * month shown. */ + if (start_date == NULL) { + calitem->selection_set = FALSE; + calitem->selection_changed = TRUE; + e_calendar_item_queue_signal_emission (calitem); + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (calitem)); + return; + } + + if (end_date == NULL) + end_date = start_date; + + g_return_if_fail (g_date_compare (start_date, end_date) <= 0); + + start_year = g_date_get_year (start_date); + start_month = g_date_get_month (start_date) - 1; + start_day = g_date_get_day (start_date); + end_year = g_date_get_year (end_date); + end_month = g_date_get_month (end_date) - 1; + end_day = g_date_get_day (end_date); + + need_update = e_calendar_item_ensure_days_visible ( + calitem, + start_year, + start_month, + start_day, + end_year, + end_month, + end_day, + emission); + + new_start_month_offset = (start_year - calitem->year) * 12 + + start_month - calitem->month; + new_start_day = start_day; + + /* This may go outside the visible months, but we don't care. */ + new_end_month_offset = (end_year - calitem->year) * 12 + + end_month - calitem->month; + new_end_day = end_day; + + if (!calitem->selection_set + || calitem->selection_start_month_offset != new_start_month_offset + || calitem->selection_start_day != new_start_day + || calitem->selection_end_month_offset != new_end_month_offset + || calitem->selection_end_day != new_end_day) { + need_update = TRUE; + if (emission) { + calitem->selection_changed = TRUE; + e_calendar_item_queue_signal_emission (calitem); + } + calitem->selection_set = TRUE; + calitem->selection_start_month_offset = new_start_month_offset; + calitem->selection_start_day = new_start_day; + calitem->selection_end_month_offset = new_end_month_offset; + calitem->selection_end_day = new_end_day; + + calitem->selection_real_start_month_offset = new_start_month_offset; + calitem->selection_real_start_day = new_start_day; + calitem->selection_from_full_week = FALSE; + } + + if (need_update) { + g_signal_emit ( + calitem, + e_calendar_item_signals[DATE_RANGE_CHANGED], 0); + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (calitem)); + } +} + +void +e_calendar_item_style_set (GtkWidget *widget, + ECalendarItem *calitem) +{ + GtkStyle *style; + GdkColor *color; + + style = gtk_widget_get_style (widget); + + color = &style->bg[GTK_STATE_SELECTED]; + calitem->colors[E_CALENDAR_ITEM_COLOR_TODAY_BOX] = *color; + + color = &style->base[GTK_STATE_NORMAL]; + calitem->colors[E_CALENDAR_ITEM_COLOR_SELECTION_FG] = *color; + + color = &style->bg[GTK_STATE_SELECTED]; + calitem->colors[E_CALENDAR_ITEM_COLOR_SELECTION_BG_FOCUSED] = *color; + + color = &style->fg[GTK_STATE_INSENSITIVE]; + calitem->colors[E_CALENDAR_ITEM_COLOR_SELECTION_BG] = *color; + + color = &style->fg[GTK_STATE_INSENSITIVE]; + calitem->colors[E_CALENDAR_ITEM_COLOR_PREV_OR_NEXT_MONTH_FG] = *color; + + e_calendar_item_recalc_sizes (calitem); +} + +void +e_calendar_item_set_selection (ECalendarItem *calitem, + const GDate *start_date, + const GDate *end_date) +{ + /* If the user is in the middle of a selection, we must abort it. */ + if (calitem->selecting) { + gnome_canvas_item_ungrab ( + GNOME_CANVAS_ITEM (calitem), + GDK_CURRENT_TIME); + calitem->selecting = FALSE; + } + + e_calendar_item_set_selection_if_emission (calitem, + start_date, end_date, + TRUE); +} + +/* This tries to ensure that the given time range is visible. If the range + * given is longer than we can show, only the start of it will be visible. + * Note that this will not update the selection. That should be done somewhere + * else. It returns TRUE if the visible range has been changed. */ +static gboolean +e_calendar_item_ensure_days_visible (ECalendarItem *calitem, + gint start_year, + gint start_month, + gint start_day, + gint end_year, + gint end_month, + gint end_day, + gboolean emission) +{ + gint current_end_year, current_end_month; + gint months_shown; + gint first_day_offset, days_in_month, days_in_prev_month; + gboolean need_update = FALSE; + + months_shown = calitem->rows * calitem->cols; + + /* Calculate the range of months currently displayed. */ + current_end_year = calitem->year; + current_end_month = calitem->month + months_shown - 1; + e_calendar_item_normalize_date ( + calitem, ¤t_end_year, + ¤t_end_month); + + /* Try to ensure that the end month is shown. */ + if ((end_year == current_end_year + 1 && + current_end_month == 11 && end_month == 0) || + (end_year == current_end_year && end_month == current_end_month + 1)) { + /* See if the end of the selection will fit in the + * leftover days of the month after the last one shown. */ + calitem->month += (months_shown - 1); + e_calendar_item_normalize_date ( + calitem, &calitem->year, + &calitem->month); + + e_calendar_item_get_month_info ( + calitem, 0, 0, + &first_day_offset, + &days_in_month, + &days_in_prev_month); + + if (end_day >= E_CALENDAR_ROWS_PER_MONTH * E_CALENDAR_COLS_PER_MONTH - + first_day_offset - days_in_month) { + need_update = TRUE; + + calitem->year = end_year; + calitem->month = end_month - months_shown + 1; + } else { + calitem->month -= (months_shown - 1); + } + + e_calendar_item_normalize_date ( + calitem, &calitem->year, + &calitem->month); + } + else if (end_year > current_end_year || + (end_year == current_end_year && end_month > current_end_month)) { + /* The selection will definitely not fit in the leftover days + * of the month after the last one shown. */ + need_update = TRUE; + + calitem->year = end_year; + calitem->month = end_month - months_shown + 1; + + e_calendar_item_normalize_date ( + calitem, &calitem->year, + &calitem->month); + } + + /* Now try to ensure that the start month is shown. We do this after + * the end month so that the start month will always be shown. */ + if (start_year < calitem->year + || (start_year == calitem->year + && start_month < calitem->month)) { + need_update = TRUE; + + /* First we see if the start of the selection will fit in the + * leftover days of the month before the first one shown. */ + calitem->year = start_year; + calitem->month = start_month + 1; + e_calendar_item_normalize_date ( + calitem, &calitem->year, + &calitem->month); + + e_calendar_item_get_month_info ( + calitem, 0, 0, + &first_day_offset, + &days_in_month, + &days_in_prev_month); + + if (start_day <= days_in_prev_month - first_day_offset) { + calitem->year = start_year; + calitem->month = start_month; + } + } + + if (need_update && emission) + e_calendar_item_date_range_changed (calitem); + + return need_update; +} + +static gboolean +destroy_menu_idle_cb (gpointer menu) +{ + gtk_widget_destroy (menu); + + return FALSE; +} + +static void +deactivate_menu_cb (GtkWidget *menu) +{ + g_signal_handlers_disconnect_by_func (menu, deactivate_menu_cb, NULL); + + g_idle_add (destroy_menu_idle_cb, menu); +} + +static void +e_calendar_item_show_popup_menu (ECalendarItem *calitem, + GdkEvent *button_event, + gint month_offset) +{ + GtkWidget *menu, *submenu, *menuitem, *label; + gint year, month; + const gchar *name; + gchar buffer[64]; + guint event_button = 0; + guint32 event_time; + + menu = gtk_menu_new (); + + for (year = calitem->year - 2; year <= calitem->year + 2; year++) { + g_snprintf (buffer, 64, "%i", year); + menuitem = gtk_menu_item_new_with_label (buffer); + gtk_widget_show (menuitem); + gtk_container_add (GTK_CONTAINER (menu), menuitem); + + submenu = gtk_menu_new (); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (menuitem), submenu); + + g_object_set_data ( + G_OBJECT (submenu), "year", + GINT_TO_POINTER (year)); + g_object_set_data ( + G_OBJECT (submenu), "month_offset", + GINT_TO_POINTER (month_offset)); + + for (month = 0; month < 12; month++) { + name = e_get_month_name (month + 1, FALSE); + + menuitem = gtk_menu_item_new (); + gtk_widget_show (menuitem); + gtk_container_add (GTK_CONTAINER (submenu), menuitem); + + label = gtk_label_new (name); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_widget_show (label); + gtk_container_add (GTK_CONTAINER (menuitem), label); + + g_object_set_data ( + G_OBJECT (menuitem), "month", + GINT_TO_POINTER (month)); + + g_signal_connect ( + menuitem, "activate", + G_CALLBACK (e_calendar_item_on_menu_item_activate), + calitem); + } + } + + g_signal_connect ( + menu, "deactivate", + G_CALLBACK (deactivate_menu_cb), NULL); + + gdk_event_get_button (button_event, &event_button); + event_time = gdk_event_get_time (button_event); + + gtk_menu_popup ( + GTK_MENU (menu), NULL, NULL, + e_calendar_item_position_menu, calitem, + event_button, event_time); +} + +static void +e_calendar_item_on_menu_item_activate (GtkWidget *menuitem, + ECalendarItem *calitem) +{ + GtkWidget *parent; + gint year, month_offset, month; + gpointer data; + + parent = gtk_widget_get_parent (menuitem); + data = g_object_get_data (G_OBJECT (parent), "year"); + year = GPOINTER_TO_INT (data); + + parent = gtk_widget_get_parent (menuitem); + data = g_object_get_data (G_OBJECT (parent), "month_offset"); + month_offset = GPOINTER_TO_INT (data); + + data = g_object_get_data (G_OBJECT (menuitem), "month"); + month = GPOINTER_TO_INT (data); + + month -= month_offset; + e_calendar_item_normalize_date (calitem, &year, &month); + e_calendar_item_set_first_month (calitem, year, month); +} + +static void +e_calendar_item_position_menu (GtkMenu *menu, + gint *x, + gint *y, + gboolean *push_in, + gpointer user_data) +{ + GtkRequisition requisition; + gint screen_width, screen_height; + + gtk_widget_get_preferred_size (GTK_WIDGET (menu), &requisition, NULL); + + *x -= (gtk_widget_get_direction(GTK_WIDGET(menu)) == GTK_TEXT_DIR_RTL) + ? requisition.width - 2 + : 2; + *y -= requisition.height / 2; + + screen_width = gdk_screen_width (); + screen_height = gdk_screen_height (); + + *x = CLAMP (*x, 0, screen_width - requisition.width); + *y = CLAMP (*y, 0, screen_height - requisition.height); +} + +/* Sets the function to call to get the colors to use for a particular day. */ +void +e_calendar_item_set_style_callback (ECalendarItem *calitem, + ECalendarItemStyleCallback cb, + gpointer data, + GDestroyNotify destroy) +{ + g_return_if_fail (E_IS_CALENDAR_ITEM (calitem)); + + if (calitem->style_callback_data && calitem->style_callback_destroy) + (*calitem->style_callback_destroy) (calitem->style_callback_data); + + calitem->style_callback = cb; + calitem->style_callback_data = data; + calitem->style_callback_destroy = destroy; +} + +static void +e_calendar_item_date_range_changed (ECalendarItem *calitem) +{ + g_free (calitem->styles); + calitem->styles = NULL; + calitem->date_range_changed = TRUE; + e_calendar_item_queue_signal_emission (calitem); +} + +static void +e_calendar_item_queue_signal_emission (ECalendarItem *calitem) +{ + if (calitem->signal_emission_idle_id == 0) { + calitem->signal_emission_idle_id = g_idle_add_full ( + G_PRIORITY_HIGH, (GSourceFunc) + e_calendar_item_signal_emission_idle_cb, + calitem, NULL); + } +} + +static gboolean +e_calendar_item_signal_emission_idle_cb (gpointer data) +{ + ECalendarItem *calitem; + + g_return_val_if_fail (E_IS_CALENDAR_ITEM (data), FALSE); + + calitem = E_CALENDAR_ITEM (data); + + calitem->signal_emission_idle_id = 0; + + /* We ref the calitem & check in case it gets destroyed, since we + * were getting a free memory write here. */ + g_object_ref ((calitem)); + + if (calitem->date_range_changed) { + calitem->date_range_changed = FALSE; + g_signal_emit (calitem, e_calendar_item_signals[DATE_RANGE_CHANGED], 0); + } + + if (calitem->selection_changed) { + calitem->selection_changed = FALSE; + g_signal_emit (calitem, e_calendar_item_signals[SELECTION_CHANGED], 0); + } + + g_object_unref ((calitem)); + + return FALSE; +} + +/* Sets a callback to use to get the current time. This is useful if the + * application needs to use its own timezone data rather than rely on the + * Unix timezone. */ +void +e_calendar_item_set_get_time_callback (ECalendarItem *calitem, + ECalendarItemGetTimeCallback cb, + gpointer data, + GDestroyNotify destroy) +{ + g_return_if_fail (E_IS_CALENDAR_ITEM (calitem)); + + if (calitem->time_callback_data && calitem->time_callback_destroy) + (*calitem->time_callback_destroy) (calitem->time_callback_data); + + calitem->time_callback = cb; + calitem->time_callback_data = data; + calitem->time_callback_destroy = destroy; +} diff --git a/e-util/e-calendar-item.h b/e-util/e-calendar-item.h new file mode 100644 index 0000000000..a4c0867b66 --- /dev/null +++ b/e-util/e-calendar-item.h @@ -0,0 +1,392 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CALENDAR_ITEM_H_ +#define _E_CALENDAR_ITEM_H_ + +#include <libgnomecanvas/gnome-canvas.h> + +G_BEGIN_DECLS + +/* + * ECalendarItem - canvas item displaying a calendar. + */ + +#define E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME 1 +#define E_CALENDAR_ITEM_YPAD_BELOW_MONTH_NAME 1 + +/* The number of rows & columns of days in each month. */ +#define E_CALENDAR_ROWS_PER_MONTH 6 +#define E_CALENDAR_COLS_PER_MONTH 7 + +/* Used to mark days as bold in e_calendar_item_mark_day(). */ +#define E_CALENDAR_ITEM_MARK_BOLD (1 << 0) +#define E_CALENDAR_ITEM_MARK_ITALIC (1 << 1) + +/* + * These are the padding sizes between various pieces of the calendar. + */ + +/* The minimum padding around the numbers in each cell/day. */ +#define E_CALENDAR_ITEM_MIN_CELL_XPAD 4 +#define E_CALENDAR_ITEM_MIN_CELL_YPAD 0 + +/* Vertical padding. */ +#define E_CALENDAR_ITEM_YPAD_ABOVE_DAY_LETTERS 1 +#define E_CALENDAR_ITEM_YPAD_BELOW_DAY_LETTERS 0 +#define E_CALENDAR_ITEM_YPAD_ABOVE_CELLS 1 +#define E_CALENDAR_ITEM_YPAD_BELOW_CELLS 2 + +/* Horizontal padding in the heading bars. */ +#define E_CALENDAR_ITEM_XPAD_BEFORE_MONTH_NAME_WITH_BUTTON 10 +#define E_CALENDAR_ITEM_XPAD_BEFORE_MONTH_NAME 3 +#define E_CALENDAR_ITEM_XPAD_AFTER_MONTH_NAME 3 +#define E_CALENDAR_ITEM_XPAD_AFTER_MONTH_NAME_WITH_BUTTON 10 + +/* Horizontal padding in the month displays. */ +#define E_CALENDAR_ITEM_XPAD_BEFORE_WEEK_NUMBERS 4 +#define E_CALENDAR_ITEM_XPAD_AFTER_WEEK_NUMBERS 2 +#define E_CALENDAR_ITEM_XPAD_BEFORE_CELLS 1 +#define E_CALENDAR_ITEM_XPAD_AFTER_CELLS 4 + +/* These index our colors array. */ +typedef enum +{ + E_CALENDAR_ITEM_COLOR_TODAY_BOX, + E_CALENDAR_ITEM_COLOR_SELECTION_FG, + E_CALENDAR_ITEM_COLOR_SELECTION_BG_FOCUSED, + E_CALENDAR_ITEM_COLOR_SELECTION_BG, + E_CALENDAR_ITEM_COLOR_PREV_OR_NEXT_MONTH_FG, + + E_CALENDAR_ITEM_COLOR_LAST +} ECalendarItemColors; + +typedef struct _ECalendarItem ECalendarItem; +typedef struct _ECalendarItemClass ECalendarItemClass; + +/* The type of the callback function optionally used to get the colors to + * use for each day. */ +typedef void (*ECalendarItemStyleCallback) (ECalendarItem *calitem, + gint year, + gint month, + gint day, + gint day_style, + gboolean today, + gboolean prev_or_next_month, + gboolean selected, + gboolean has_focus, + gboolean drop_target, + GdkColor **bg_color, + GdkColor **fg_color, + GdkColor **box_color, + gboolean *bold, + gboolean *italic, + gpointer data); + +/* The type of the callback function optionally used to get the current time. + */ +typedef struct tm (*ECalendarItemGetTimeCallback) (ECalendarItem *calitem, + gpointer data); + +/* Standard GObject macros */ +#define E_TYPE_CALENDAR_ITEM \ + (e_calendar_item_get_type ()) +#define E_CALENDAR_ITEM(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CALENDAR_ITEM, ECalendarItem)) +#define E_CALENDAR_ITEM_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CALENDAR_ITEM, ECalendarItemClass)) +#define E_IS_CALENDAR_ITEM(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CALENDAR_ITEM)) +#define E_IS_CALENDAR_ITEM_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CALENDAR_ITEM)) +#define E_CALENDAR_ITEM_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CALENDAR_ITEM, ECalendarItemClass)) + +struct _ECalendarItem { + GnomeCanvasItem canvas_item; + + /* The year & month of the first calendar being displayed. */ + gint year; + gint month; /* 0 to 11 */ + + /* Points to an array of styles, one gchar for each day. We use 32 + * chars for each month, with n + 2 months, where n is the number of + * complete months shown (since we show some days before the first + * month and after the last month grayes out). + * A value of 0 is the default, and 1 is bold. */ + guint8 *styles; + + /* + * Options. + */ + + /* The minimum & maximum number of rows & columns of months. + * If the maximum values are -1 then there is no maximum. + * The minimum valies default to 1. The maximum values to -1. */ + gint min_rows; + gint min_cols; + gint max_rows; + gint max_cols; + + /* The actual number of rows & columns of months. */ + gint rows; + gint cols; + + /* Whether we show week nubers. */ + gboolean show_week_numbers; + /* whether to keep same week days selected on week number click */ + gboolean keep_wdays_on_weeknum_click; + + /* The first day of the week, 0 (Monday) to 6 (Sunday). */ + gint week_start_day; + + /* Whether the cells expand to fill extra space. */ + gboolean expand; + + /* The maximum number of days that can be selected. Defaults to 1. */ + gint max_days_selected; + + /* The number of days selected before we switch to selecting whole + * weeks, or -1 if we never switch. Defaults to -1. */ + gint days_to_start_week_selection; + + /* Whether the selection is moved when we move back/forward one month. + * Used for things like the EDateEdit which only want the selection to + * be changed when the user explicitly selects a day. */ + gboolean move_selection_when_moving; + + /* Whether the selection day is preserved when we move back/forward + * one month. Used for the work week and week view. */ + gboolean preserve_day_when_moving; + + /* Whether to display the pop-up, TRUE by default */ + gboolean display_popup; + + /* + * Internal stuff. + */ + + /* Bounds of item. */ + gdouble x1, y1, x2, y2; + + /* The minimum size of each month, based on the fonts used. */ + gint min_month_width; + gint min_month_height; + + /* The actual size of each month, after dividing extra space. */ + gint month_width; + gint month_height; + + /* The offset to the left edge of the first calendar. */ + gint x_offset; + + /* The padding around each calendar month. */ + gint month_lpad, month_rpad; + gint month_tpad, month_bpad; + + /* The size of each cell. */ + gint cell_width; + gint cell_height; + + /* The current selection. The month offsets are from 0, which is the + * top-left calendar month view. Note that -1 is used for the last days + * from the previous month. The days are real month days. */ + gboolean selecting; + GDate *selecting_axis; + gboolean selection_dragging_end; + gboolean selection_from_full_week; + gboolean selection_set; + gint selection_start_month_offset; + gint selection_start_day; + gint selection_end_month_offset; + gint selection_end_day; + gint selection_real_start_month_offset; + gint selection_real_start_day; + + /* Widths of the day characters. */ + gint day_widths[7]; + gint max_day_width; + + /* Widths of the digits, '0' .. '9'. */ + gint digit_widths[10]; + gint max_digit_width; + + gint week_number_digit_widths[10]; + gint max_week_number_digit_width; + + gint max_month_name_width; + + /* Fonts for drawing text. If font isn't set it uses the font from the + * canvas widget. If week_number_font isn't set it uses font. */ + PangoFontDescription *font_desc; + PangoFontDescription *week_number_font_desc; + + ECalendarItemStyleCallback style_callback; + gpointer style_callback_data; + GDestroyNotify style_callback_destroy; + + ECalendarItemGetTimeCallback time_callback; + gpointer time_callback_data; + GDestroyNotify time_callback_destroy; + + /* Colors for drawing. */ + GdkColor colors[E_CALENDAR_ITEM_COLOR_LAST]; + + /* Our idle handler for emitting signals. */ + gint signal_emission_idle_id; + + /* A flag to indicate that the selection or date range has changed. + * When set the idle function will emit the signal and reset it to + * FALSE. This is so we don't emit it several times when args are set + * etc. */ + gboolean selection_changed; + gboolean date_range_changed; +}; + +struct _ECalendarItemClass { + GnomeCanvasItemClass parent_class; + + void (* date_range_changed) (ECalendarItem *calitem); + void (* selection_changed) (ECalendarItem *calitem); + void (* selection_preview_changed) (ECalendarItem *calitem); +}; + +GType e_calendar_item_get_type (void); + +/* FIXME: months are 0-11 throughout, but 1-12 may be better. */ + +void e_calendar_item_get_first_month (ECalendarItem *calitem, + gint *year, + gint *month); +void e_calendar_item_set_first_month (ECalendarItem *calitem, + gint year, + gint month); + +/* Get the maximum number of days selectable */ +gint e_calendar_item_get_max_days_sel (ECalendarItem *calitem); + +/* Set the maximum number of days selectable */ +void e_calendar_item_set_max_days_sel (ECalendarItem *calitem, + gint days); + +/* Get the maximum number of days selectable */ +gint e_calendar_item_get_days_start_week_sel (ECalendarItem *calitem); + +/* Set the maximum number of days selectable */ +void e_calendar_item_set_days_start_week_sel (ECalendarItem *calitem, + gint days); + +/* Set the maximum number of days before whole weeks are selected */ +gboolean + e_calendar_item_get_display_popup (ECalendarItem *calitem); + +/* Get the maximum number of days before whole weeks are selected */ +void e_calendar_item_set_display_popup (ECalendarItem *calitem, + gboolean display); + +/* Gets the range of dates actually shown. Months are 0 to 11. + * This also includes the last days of the previous month and the first days + * of the following month, which are normally shown in gray. + * It returns FALSE if no dates are currently shown. */ +gboolean + e_calendar_item_get_date_range (ECalendarItem *calitem, + gint *start_year, + gint *start_month, + gint *start_day, + gint *end_year, + gint *end_month, + gint *end_day); + +/* Returns the selected date range. It returns FALSE if no days are currently + * selected. */ +gboolean + e_calendar_item_get_selection (ECalendarItem *calitem, + GDate *start_date, + GDate *end_date); +/* Sets the selected date range, and changes the date range shown so at least + * the start of the selection is shown. If start_date is NULL it clears the + * selection. */ +void e_calendar_item_set_selection (ECalendarItem *calitem, + const GDate *start_date, + const GDate *end_date); + +/* Marks a particular day. Passing E_CALENDAR_ITEM_MARK_BOLD as the day style + * will result in the day being shown as bold by default. The style callback + * could support more day_styles, or the style callback could determine the + * colors itself, without needing to mark days. */ +void e_calendar_item_clear_marks (ECalendarItem *calitem); +void e_calendar_item_mark_day (ECalendarItem *calitem, + gint year, + gint month, + gint day, + guint8 day_style, + gboolean add_day_style); + +/* Mark a range of days. Any days outside the currently shown range are + * ignored. */ +void e_calendar_item_mark_days (ECalendarItem *calitem, + gint start_year, + gint start_month, + gint start_day, + gint end_year, + gint end_month, + gint end_day, + guint8 day_style, + gboolean add_day_style); + +/* Sets the function to call to get the colors to use for a particular day. */ +void e_calendar_item_set_style_callback (ECalendarItem *calitem, + ECalendarItemStyleCallback cb, + gpointer data, + GDestroyNotify destroy); + +/* Sets a callback to use to get the current time. This is useful if the + * application needs to use its own timezone data rather than rely on the + * Unix timezone. */ +void e_calendar_item_set_get_time_callback (ECalendarItem *calitem, + ECalendarItemGetTimeCallback cb, + gpointer data, + GDestroyNotify destroy); +void e_calendar_item_normalize_date (ECalendarItem *calitem, + gint *year, + gint *month); +gint e_calendar_item_get_week_number (ECalendarItem *calitem, + gint day, + gint month, + gint year); +void e_calendar_item_style_set (GtkWidget *widget, + ECalendarItem *calitem); + +G_END_DECLS + +#endif /* _E_CALENDAR_ITEM_H_ */ diff --git a/e-util/e-calendar.c b/e-util/e-calendar.c new file mode 100644 index 0000000000..38336cb618 --- /dev/null +++ b/e-util/e-calendar.c @@ -0,0 +1,848 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * ECalendar - displays a table of monthly calendars, allowing highlighting + * and selection of one or more days. Like GtkCalendar with more features. + * Most of the functionality is in the ECalendarItem canvas item, though + * we also add GnomeCanvasWidget buttons to go to the previous/next month and + * to got to the current day. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-calendar.h" + +#include <gtk/gtk.h> +#include <libgnomecanvas/gnome-canvas-widget.h> +#include <glib/gi18n.h> + +#define E_CALENDAR_SMALL_FONT_PTSIZE 6 + +#define E_CALENDAR_SMALL_FONT \ + "-adobe-utopia-regular-r-normal-*-*-100-*-*-p-*-iso8859-*" +#define E_CALENDAR_SMALL_FONT_FALLBACK \ + "-adobe-helvetica-medium-r-normal-*-*-80-*-*-p-*-iso8859-*" + +/* The space between the arrow buttons and the edge of the widget. */ +#define E_CALENDAR_ARROW_BUTTON_X_PAD 2 +#define E_CALENDAR_ARROW_BUTTON_Y_PAD 0 + +/* Vertical padding. The padding above the button includes the space for the + * horizontal line. */ +#define E_CALENDAR_YPAD_ABOVE_LOWER_BUTTONS 4 +#define E_CALENDAR_YPAD_BELOW_LOWER_BUTTONS 3 + +/* Horizontal padding inside & between buttons. */ +#define E_CALENDAR_IXPAD_BUTTONS 4 +#define E_CALENDAR_XPAD_BUTTONS 8 + +/* The time between steps when the prev/next buttons is pressed, in 1/1000ths + * of a second, and the number of timeouts we skip before we start + * automatically moving back/forward. */ +#define E_CALENDAR_AUTO_MOVE_TIMEOUT 150 +#define E_CALENDAR_AUTO_MOVE_TIMEOUT_DELAY 2 + +static void e_calendar_dispose (GObject *object); +static void e_calendar_realize (GtkWidget *widget); +static void e_calendar_style_set (GtkWidget *widget, + GtkStyle *previous_style); +static void e_calendar_get_preferred_width (GtkWidget *widget, + gint *minimal_width, + gint *natural_width); +static void e_calendar_get_preferred_height (GtkWidget *widget, + gint *minimal_height, + gint *natural_height); +static void e_calendar_size_allocate (GtkWidget *widget, + GtkAllocation *allocation); +static gint e_calendar_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time); +static void e_calendar_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time); +static gboolean e_calendar_button_has_focus (ECalendar *cal); +static gboolean e_calendar_focus (GtkWidget *widget, + GtkDirectionType direction); + +static void e_calendar_on_prev_pressed (ECalendar *cal); +static void e_calendar_on_prev_released (ECalendar *cal); +static void e_calendar_on_prev_clicked (ECalendar *cal); +static void e_calendar_on_next_pressed (ECalendar *cal); +static void e_calendar_on_next_released (ECalendar *cal); +static void e_calendar_on_next_clicked (ECalendar *cal); +static void e_calendar_on_prev_year_pressed (ECalendar *cal); +static void e_calendar_on_prev_year_released (ECalendar *cal); +static void e_calendar_on_prev_year_clicked (ECalendar *cal); +static void e_calendar_on_next_year_pressed (ECalendar *cal); +static void e_calendar_on_next_year_released (ECalendar *cal); +static void e_calendar_on_next_year_clicked (ECalendar *cal); + +static void e_calendar_start_auto_move (ECalendar *cal, + gboolean moving_forward); +static gboolean e_calendar_auto_move_handler (gpointer data); +static void e_calendar_start_auto_move_year (ECalendar *cal, + gboolean moving_forward); +static gboolean e_calendar_auto_move_year_handler (gpointer data); +static void e_calendar_stop_auto_move (ECalendar *cal); + +G_DEFINE_TYPE ( + ECalendar, + e_calendar, + E_TYPE_CANVAS) + +static void +e_calendar_class_init (ECalendarClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + object_class = (GObjectClass *) class; + widget_class = (GtkWidgetClass *) class; + + object_class->dispose = e_calendar_dispose; + + widget_class->realize = e_calendar_realize; + widget_class->style_set = e_calendar_style_set; + widget_class->get_preferred_width = e_calendar_get_preferred_width; + widget_class->get_preferred_height = e_calendar_get_preferred_height; + widget_class->size_allocate = e_calendar_size_allocate; + widget_class->drag_motion = e_calendar_drag_motion; + widget_class->drag_leave = e_calendar_drag_leave; + widget_class->focus = e_calendar_focus; +} + +static void +e_calendar_init (ECalendar *cal) +{ + GnomeCanvasGroup *canvas_group; + PangoFontDescription *small_font_desc; + GtkWidget *button, *pixmap; + AtkObject *a11y; + + /* Create the small font. */ + + small_font_desc = pango_font_description_copy ( + gtk_widget_get_style (GTK_WIDGET (cal))->font_desc); + pango_font_description_set_size ( + small_font_desc, + E_CALENDAR_SMALL_FONT_PTSIZE * PANGO_SCALE); + + canvas_group = GNOME_CANVAS_GROUP (GNOME_CANVAS (cal)->root); + + cal->calitem = E_CALENDAR_ITEM ( + gnome_canvas_item_new ( + canvas_group, + e_calendar_item_get_type (), + "week_number_font_desc", small_font_desc, + NULL)); + + pango_font_description_free (small_font_desc); + + /* Create the arrow buttons to move to the previous/next month. */ + button = gtk_button_new (); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + gtk_widget_show (button); + g_signal_connect_swapped ( + button, "pressed", + G_CALLBACK (e_calendar_on_prev_pressed), cal); + g_signal_connect_swapped ( + button, "released", + G_CALLBACK (e_calendar_on_prev_released), cal); + g_signal_connect_swapped ( + button, "clicked", + G_CALLBACK (e_calendar_on_prev_clicked), cal); + + pixmap = gtk_arrow_new (GTK_ARROW_LEFT, GTK_SHADOW_NONE); + gtk_widget_show (pixmap); + gtk_container_add (GTK_CONTAINER (button), pixmap); + + cal->prev_item = gnome_canvas_item_new ( + canvas_group, + gnome_canvas_widget_get_type (), + "widget", button, + NULL); + a11y = gtk_widget_get_accessible (button); + atk_object_set_name (a11y, _("Previous month")); + + button = gtk_button_new (); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + gtk_widget_show (button); + g_signal_connect_swapped ( + button, "pressed", + G_CALLBACK (e_calendar_on_next_pressed), cal); + g_signal_connect_swapped ( + button, "released", + G_CALLBACK (e_calendar_on_next_released), cal); + g_signal_connect_swapped ( + button, "clicked", + G_CALLBACK (e_calendar_on_next_clicked), cal); + + pixmap = gtk_arrow_new (GTK_ARROW_RIGHT, GTK_SHADOW_NONE); + gtk_widget_show (pixmap); + gtk_container_add (GTK_CONTAINER (button), pixmap); + + cal->next_item = gnome_canvas_item_new ( + canvas_group, + gnome_canvas_widget_get_type (), + "widget", button, + NULL); + a11y = gtk_widget_get_accessible (button); + atk_object_set_name (a11y, _("Next month")); + + /* Create the arrow buttons to move to the previous/next year. */ + button = gtk_button_new (); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + gtk_widget_show (button); + g_signal_connect_swapped ( + button, "pressed", + G_CALLBACK (e_calendar_on_prev_year_pressed), cal); + g_signal_connect_swapped ( + button, "released", + G_CALLBACK (e_calendar_on_prev_year_released), cal); + g_signal_connect_swapped ( + button, "clicked", + G_CALLBACK (e_calendar_on_prev_year_clicked), cal); + + pixmap = gtk_arrow_new (GTK_ARROW_LEFT, GTK_SHADOW_NONE); + gtk_widget_show (pixmap); + gtk_container_add (GTK_CONTAINER (button), pixmap); + + cal->prev_item_year = gnome_canvas_item_new ( + canvas_group, + gnome_canvas_widget_get_type (), + "widget", button, + NULL); + a11y = gtk_widget_get_accessible (button); + atk_object_set_name (a11y, _("Previous year")); + + button = gtk_button_new (); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + gtk_widget_show (button); + g_signal_connect_swapped ( + button, "pressed", + G_CALLBACK (e_calendar_on_next_year_pressed), cal); + g_signal_connect_swapped ( + button, "released", + G_CALLBACK (e_calendar_on_next_year_released), cal); + g_signal_connect_swapped ( + button, "clicked", + G_CALLBACK (e_calendar_on_next_year_clicked), cal); + + pixmap = gtk_arrow_new (GTK_ARROW_RIGHT, GTK_SHADOW_NONE); + gtk_widget_show (pixmap); + gtk_container_add (GTK_CONTAINER (button), pixmap); + + cal->next_item_year = gnome_canvas_item_new ( + canvas_group, + gnome_canvas_widget_get_type (), + "widget", button, + NULL); + a11y = gtk_widget_get_accessible (button); + atk_object_set_name (a11y, _("Next year")); + + cal->min_rows = 1; + cal->min_cols = 1; + cal->max_rows = -1; + cal->max_cols = -1; + + cal->timeout_id = 0; +} + +/** + * e_calendar_new: + * @Returns: a new #ECalendar. + * + * Creates a new #ECalendar. + **/ +GtkWidget * +e_calendar_new (void) +{ + GtkWidget *cal; + AtkObject *a11y; + + cal = g_object_new (e_calendar_get_type (), NULL); + a11y = gtk_widget_get_accessible (cal); + atk_object_set_name (a11y, _("Month Calendar")); + + return cal; +} + +static void +e_calendar_dispose (GObject *object) +{ + ECalendar *cal; + + g_return_if_fail (object != NULL); + g_return_if_fail (E_IS_CALENDAR (object)); + + cal = E_CALENDAR (object); + + if (cal->timeout_id != 0) { + g_source_remove (cal->timeout_id); + cal->timeout_id = 0; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_calendar_parent_class)->dispose (object); +} + +static void +e_calendar_realize (GtkWidget *widget) +{ + GtkStyle *style; + GdkWindow *window; + + (*GTK_WIDGET_CLASS (e_calendar_parent_class)->realize) (widget); + + /* Set the background of the canvas window to the normal color, + * or the arrow buttons are not displayed properly. */ + style = gtk_widget_get_style (widget); + window = gtk_layout_get_bin_window (GTK_LAYOUT (widget)); + gdk_window_set_background (window, &style->bg[GTK_STATE_NORMAL]); +} + +static void +e_calendar_style_set (GtkWidget *widget, + GtkStyle *previous_style) +{ + ECalendar *e_calendar; + + e_calendar = E_CALENDAR (widget); + if (GTK_WIDGET_CLASS (e_calendar_parent_class)->style_set) + (*GTK_WIDGET_CLASS (e_calendar_parent_class)->style_set) (widget, + previous_style); + + /* Set the background of the canvas window to the normal color, + * or the arrow buttons are not displayed properly. */ + if (gtk_widget_get_realized (widget)) { + GtkStyle *style; + GdkWindow *window; + + style = gtk_widget_get_style (widget); + window = gtk_layout_get_bin_window (GTK_LAYOUT (widget)); + gdk_window_set_background (window, &style->bg[GTK_STATE_NORMAL]); + } + e_calendar_item_style_set (widget, e_calendar->calitem); +} + +static void +e_calendar_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + ECalendar *cal; + GtkStyle *style; + gint col_width; + + cal = E_CALENDAR (widget); + style = gtk_widget_get_style (GTK_WIDGET (cal)); + + g_object_get ((cal->calitem), "column_width", &col_width, NULL); + + *minimum = *natural = col_width * cal->min_cols + style->xthickness * 2; +} + +static void +e_calendar_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + ECalendar *cal; + GtkStyle *style; + gint row_height; + + cal = E_CALENDAR (widget); + style = gtk_widget_get_style (GTK_WIDGET (cal)); + + g_object_get ((cal->calitem), "row_height", &row_height, NULL); + + *minimum = *natural = row_height * cal->min_rows + style->ythickness * 2; +} + +static void +e_calendar_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + ECalendar *cal; + GtkStyle *style; + GtkAllocation old_allocation; + PangoFontDescription *font_desc; + PangoContext *pango_context; + PangoFontMetrics *font_metrics; + gdouble old_x2, old_y2, new_x2, new_y2; + gdouble xthickness, ythickness, arrow_button_size, current_x, month_width; + gboolean is_rtl; + + cal = E_CALENDAR (widget); + style = gtk_widget_get_style (widget); + xthickness = style->xthickness; + ythickness = style->ythickness; + + (*GTK_WIDGET_CLASS (e_calendar_parent_class)->size_allocate) (widget, allocation); + + /* Set up Pango prerequisites */ + font_desc = gtk_widget_get_style (widget)->font_desc; + pango_context = gtk_widget_get_pango_context (widget); + font_metrics = pango_context_get_metrics ( + pango_context, font_desc, + pango_context_get_language (pango_context)); + + /* Set the scroll region to its allocated size, if changed. */ + gnome_canvas_get_scroll_region ( + GNOME_CANVAS (cal), + NULL, NULL, &old_x2, &old_y2); + gtk_widget_get_allocation (widget, &old_allocation); + new_x2 = old_allocation.width - 1; + new_y2 = old_allocation.height - 1; + if (old_x2 != new_x2 || old_y2 != new_y2) + gnome_canvas_set_scroll_region ( + GNOME_CANVAS (cal), + 0, 0, new_x2, new_y2); + + /* Take off space for line & buttons if shown. */ + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (cal->calitem), + "x1", 0.0, + "y1", 0.0, + "x2", new_x2, + "y2", new_y2, + NULL); + + if (cal->calitem->month_width > 0) + month_width = cal->calitem->month_width; + else + month_width = new_x2; + month_width -= E_CALENDAR_ITEM_MIN_CELL_XPAD + E_CALENDAR_ARROW_BUTTON_X_PAD; + + /* Position the arrow buttons. */ + arrow_button_size = + PANGO_PIXELS (pango_font_metrics_get_ascent (font_metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (font_metrics)) + + E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME + + E_CALENDAR_ITEM_YPAD_BELOW_MONTH_NAME + - E_CALENDAR_ARROW_BUTTON_Y_PAD * 2 - 2; + + is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL; + current_x = is_rtl ? + (month_width - 2 * xthickness - E_CALENDAR_ARROW_BUTTON_X_PAD - arrow_button_size) : + (xthickness); + + gnome_canvas_item_set ( + cal->prev_item, + "x", current_x, + "y", ythickness + E_CALENDAR_ARROW_BUTTON_Y_PAD, + "width", arrow_button_size, + "height", arrow_button_size, + NULL); + + current_x += (is_rtl ? -1.0 : +1.0) * (cal->calitem->max_month_name_width - xthickness + 2 * arrow_button_size); + + gnome_canvas_item_set ( + cal->next_item, + "x", current_x, + "y", ythickness + E_CALENDAR_ARROW_BUTTON_Y_PAD, + "width", arrow_button_size, + "height", arrow_button_size, + NULL); + + current_x = is_rtl ? + (xthickness) : + (month_width - 2 * xthickness - E_CALENDAR_ARROW_BUTTON_X_PAD - arrow_button_size); + + gnome_canvas_item_set ( + cal->next_item_year, + "x", current_x, + "y", ythickness + E_CALENDAR_ARROW_BUTTON_Y_PAD, + "width", arrow_button_size, + "height", arrow_button_size, + NULL); + + current_x += (is_rtl ? +1.0 : -1.0) * (cal->calitem->max_digit_width * 5 - xthickness + 2 * arrow_button_size); + + gnome_canvas_item_set ( + cal->prev_item_year, + "x", current_x, + "y", ythickness + E_CALENDAR_ARROW_BUTTON_Y_PAD, + "width", arrow_button_size, + "height", arrow_button_size, + NULL); + + pango_font_metrics_unref (font_metrics); +} + +void +e_calendar_set_minimum_size (ECalendar *cal, + gint rows, + gint cols) +{ + g_return_if_fail (E_IS_CALENDAR (cal)); + + cal->min_rows = rows; + cal->min_cols = cols; + + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (cal->calitem), + "minimum_rows", rows, + "minimum_columns", cols, + NULL); + + gtk_widget_queue_resize (GTK_WIDGET (cal)); +} + +void +e_calendar_set_maximum_size (ECalendar *cal, + gint rows, + gint cols) +{ + g_return_if_fail (E_IS_CALENDAR (cal)); + + cal->max_rows = rows; + cal->max_cols = cols; + + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (cal->calitem), + "maximum_rows", rows, + "maximum_columns", cols, + NULL); + + gtk_widget_queue_resize (GTK_WIDGET (cal)); +} + +/* Returns the border size on each side of the month displays. */ +void +e_calendar_get_border_size (ECalendar *cal, + gint *top, + gint *bottom, + gint *left, + gint *right) +{ + GtkStyle *style; + + g_return_if_fail (E_IS_CALENDAR (cal)); + + style = gtk_widget_get_style (GTK_WIDGET (cal)); + + if (style) { + *top = style->ythickness; + *bottom = style->ythickness; + *left = style->xthickness; + *right = style->xthickness; + } else { + *top = *bottom = *left = *right = 0; + } +} + +static void +e_calendar_on_prev_pressed (ECalendar *cal) +{ + e_calendar_start_auto_move (cal, FALSE); +} + +static void +e_calendar_on_next_pressed (ECalendar *cal) +{ + e_calendar_start_auto_move (cal, TRUE); +} + +static void +e_calendar_on_prev_year_pressed (ECalendar *cal) +{ + e_calendar_start_auto_move_year (cal, FALSE); +} + +static void +e_calendar_on_next_year_pressed (ECalendar *cal) +{ + e_calendar_start_auto_move_year (cal, TRUE); +} + +static void +e_calendar_start_auto_move (ECalendar *cal, + gboolean moving_forward) +{ + if (cal->timeout_id == 0) { + cal->timeout_id = g_timeout_add ( + E_CALENDAR_AUTO_MOVE_TIMEOUT, + e_calendar_auto_move_handler, + cal); + } + cal->timeout_delay = E_CALENDAR_AUTO_MOVE_TIMEOUT_DELAY; + cal->moving_forward = moving_forward; + +} + +static void +e_calendar_start_auto_move_year (ECalendar *cal, + gboolean moving_forward) +{ + if (cal->timeout_id == 0) { + cal->timeout_id = g_timeout_add ( + E_CALENDAR_AUTO_MOVE_TIMEOUT, + e_calendar_auto_move_year_handler, + cal); + } + cal->timeout_delay = E_CALENDAR_AUTO_MOVE_TIMEOUT_DELAY; + cal->moving_forward = moving_forward; +} + +static gboolean +e_calendar_auto_move_year_handler (gpointer data) +{ + ECalendar *cal; + ECalendarItem *calitem; + gint offset; + + g_return_val_if_fail (E_IS_CALENDAR (data), FALSE); + + cal = E_CALENDAR (data); + calitem = cal->calitem; + + if (cal->timeout_delay > 0) { + cal->timeout_delay--; + } else { + offset = cal->moving_forward ? 12 : -12; + e_calendar_item_set_first_month ( + calitem, calitem->year, + calitem->month + offset); + } + + return TRUE; +} + +static gboolean +e_calendar_auto_move_handler (gpointer data) +{ + ECalendar *cal; + ECalendarItem *calitem; + gint offset; + + g_return_val_if_fail (E_IS_CALENDAR (data), FALSE); + + cal = E_CALENDAR (data); + calitem = cal->calitem; + + if (cal->timeout_delay > 0) { + cal->timeout_delay--; + } else { + offset = cal->moving_forward ? 1 : -1; + e_calendar_item_set_first_month ( + calitem, calitem->year, + calitem->month + offset); + } + + return TRUE; +} + +static void +e_calendar_on_prev_released (ECalendar *cal) +{ + e_calendar_stop_auto_move (cal); +} + +static void +e_calendar_on_next_released (ECalendar *cal) +{ + e_calendar_stop_auto_move (cal); +} + +static void +e_calendar_on_prev_year_released (ECalendar *cal) +{ + e_calendar_stop_auto_move (cal); +} + +static void +e_calendar_on_next_year_released (ECalendar *cal) +{ + e_calendar_stop_auto_move (cal); +} + +static void +e_calendar_stop_auto_move (ECalendar *cal) +{ + if (cal->timeout_id != 0) { + g_source_remove (cal->timeout_id); + cal->timeout_id = 0; + } +} + +static void +e_calendar_on_prev_clicked (ECalendar *cal) +{ + e_calendar_item_set_first_month ( + cal->calitem, cal->calitem->year, + cal->calitem->month - 1); +} + +static void +e_calendar_on_next_clicked (ECalendar *cal) +{ + e_calendar_item_set_first_month ( + cal->calitem, cal->calitem->year, + cal->calitem->month + 1); +} + +static void +e_calendar_on_prev_year_clicked (ECalendar *cal) +{ + e_calendar_item_set_first_month ( + cal->calitem, cal->calitem->year, + cal->calitem->month - 12); +} + +static void +e_calendar_on_next_year_clicked (ECalendar *cal) +{ + e_calendar_item_set_first_month ( + cal->calitem, cal->calitem->year, + cal->calitem->month + 12); +} + +static gint +e_calendar_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + return FALSE; +} + +static void +e_calendar_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time) +{ +} + +static gboolean +e_calendar_button_has_focus (ECalendar *cal) +{ + GtkWidget *prev_widget, *next_widget; + gboolean ret_val; + + g_return_val_if_fail (E_IS_CALENDAR (cal), FALSE); + + prev_widget = GNOME_CANVAS_WIDGET (cal->prev_item)->widget; + next_widget = GNOME_CANVAS_WIDGET (cal->next_item)->widget; + ret_val = gtk_widget_has_focus (prev_widget) || + gtk_widget_has_focus (next_widget); + return ret_val; +} + +static gboolean +e_calendar_focus (GtkWidget *widget, + GtkDirectionType direction) +{ +#define E_CALENDAR_FOCUS_CHILDREN_NUM 5 + ECalendar *cal; + GnomeCanvas *canvas; + GnomeCanvasItem *children[E_CALENDAR_FOCUS_CHILDREN_NUM]; + gint focused_index = -1; + gint index; + + g_return_val_if_fail (widget != NULL, FALSE); + g_return_val_if_fail (E_IS_CALENDAR (widget), FALSE); + cal = E_CALENDAR (widget); + canvas = GNOME_CANVAS (widget); + + if (!gtk_widget_get_can_focus (widget)) + return FALSE; + + children[0] = GNOME_CANVAS_ITEM (cal->calitem); + children[1] = cal->prev_item; + children[2] = cal->next_item; + children[3] = cal->prev_item_year; + children[4] = cal->next_item_year; + + /* get current focused item, if e-calendar has had focus */ + if (gtk_widget_has_focus (widget) || e_calendar_button_has_focus (cal)) + for (index = 0; index < E_CALENDAR_FOCUS_CHILDREN_NUM; ++index) { + if (canvas->focused_item == NULL) + break; + + if (children[index] == canvas->focused_item) { + focused_index = index; + break; + } + } + + if (focused_index == -1) + if (direction == GTK_DIR_TAB_FORWARD) + focused_index = 0; + else + focused_index = E_CALENDAR_FOCUS_CHILDREN_NUM - 1; + else + if (direction == GTK_DIR_TAB_FORWARD) + ++focused_index; + else + --focused_index; + + if (focused_index < 0 || + focused_index >= E_CALENDAR_FOCUS_CHILDREN_NUM) + /* move out of e-calendar */ + return FALSE; + gnome_canvas_item_grab_focus (children[focused_index]); + if (GNOME_IS_CANVAS_WIDGET (children[focused_index])) { + widget = GNOME_CANVAS_WIDGET (children[focused_index])->widget; + gtk_widget_grab_focus (widget); + } + return TRUE; +} + +void +e_calendar_set_focusable (ECalendar *cal, + gboolean focusable) +{ + GtkWidget *widget; + GtkWidget *prev_widget, *next_widget; + GtkWidget *toplevel; + + g_return_if_fail (E_IS_CALENDAR (cal)); + + widget = GTK_WIDGET (cal); + prev_widget = GNOME_CANVAS_WIDGET (cal->prev_item)->widget; + next_widget = GNOME_CANVAS_WIDGET (cal->next_item)->widget; + + if (focusable) { + gtk_widget_set_can_focus (widget, TRUE); + gtk_widget_set_can_focus (prev_widget, TRUE); + gtk_widget_set_can_focus (next_widget, TRUE); + } + else { + if (gtk_widget_has_focus (GTK_WIDGET (cal)) || + e_calendar_button_has_focus (cal)) { + toplevel = gtk_widget_get_toplevel (widget); + if (toplevel) + gtk_widget_grab_focus (toplevel); + } + gtk_widget_set_can_focus (widget, FALSE); + gtk_widget_set_can_focus (prev_widget, FALSE); + gtk_widget_set_can_focus (next_widget, FALSE); + } +} diff --git a/e-util/e-calendar.h b/e-util/e-calendar.h new file mode 100644 index 0000000000..9a3651348c --- /dev/null +++ b/e-util/e-calendar.h @@ -0,0 +1,112 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CALENDAR_H_ +#define _E_CALENDAR_H_ + +#include <gtk/gtk.h> +#include <e-util/e-canvas.h> +#include <e-util/e-calendar-item.h> + +G_BEGIN_DECLS + +/* + * ECalendar - displays a table of monthly calendars, allowing highlighting + * and selection of one or more days. Like GtkCalendar with more features. + * Most of the functionality is in the ECalendarItem canvas item, though + * we also add GnomeCanvasWidget buttons to go to the previous/next month and + * to got to the current day. + */ + +/* Standard GObject macros */ +#define E_TYPE_CALENDAR \ + (e_calendar_get_type ()) +#define E_CALENDAR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CALENDAR, ECalendar)) +#define E_CALENDAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CALENDAR, ECalendarClass)) +#define E_IS_CALENDAR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CALENDAR)) +#define E_IS_CALENDAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CALENDAR)) +#define E_CALENDAR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CALENDAR, ECalendarClass)) + +typedef struct _ECalendar ECalendar; +typedef struct _ECalendarClass ECalendarClass; + +struct _ECalendar { + ECanvas parent; + + ECalendarItem *calitem; + + GnomeCanvasItem *prev_item; + GnomeCanvasItem *next_item; + GnomeCanvasItem *prev_item_year; + GnomeCanvasItem *next_item_year; + + gint min_rows; + gint min_cols; + + gint max_rows; + gint max_cols; + + /* These are all used when the prev/next buttons are held down. + * moving_forward is TRUE if we are moving forward in time, i.e. the + * next button is pressed. */ + gint timeout_id; + gint timeout_delay; + gboolean moving_forward; +}; + +struct _ECalendarClass { + ECanvasClass parent_class; +}; + +GType e_calendar_get_type (void); +GtkWidget * e_calendar_new (void); +void e_calendar_set_minimum_size (ECalendar *cal, + gint rows, + gint cols); +void e_calendar_set_maximum_size (ECalendar *cal, + gint rows, + gint cols); +void e_calendar_get_border_size (ECalendar *cal, + gint *top, + gint *bottom, + gint *left, + gint *right); +void e_calendar_set_focusable (ECalendar *cal, + gboolean focusable); + +G_END_DECLS + +#endif /* _E_CALENDAR_H_ */ diff --git a/e-util/e-canvas-background.c b/e-util/e-canvas-background.c new file mode 100644 index 0000000000..6379697719 --- /dev/null +++ b/e-util/e-canvas-background.c @@ -0,0 +1,279 @@ +/* + * e-canvas-background.c - background color for canvas. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-canvas-background.h" + +#include <math.h> +#include <stdio.h> +#include <string.h> + +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> + +#include <glib/gi18n.h> + +#include "e-canvas.h" +#include "e-canvas-utils.h" + +#define E_CANVAS_BACKGROUND_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CANVAS_BACKGROUND, ECanvasBackgroundPrivate)) + +/* workaround for avoiding API broken */ +#define ecb_get_type e_canvas_background_get_type +G_DEFINE_TYPE ( + ECanvasBackground, + ecb, + GNOME_TYPE_CANVAS_ITEM) + +#define d(x) + +struct _ECanvasBackgroundPrivate { + guint rgba; /* Fill color, RGBA */ +}; + +enum { + STYLE_SET, + LAST_SIGNAL +}; + +static guint ecb_signals[LAST_SIGNAL] = { 0, }; + +enum { + PROP_0, + PROP_FILL_COLOR, + PROP_FILL_COLOR_GDK, + PROP_FILL_COLOR_RGBA, +}; + +static void +ecb_bounds (GnomeCanvasItem *item, + gdouble *x1, + gdouble *y1, + gdouble *x2, + gdouble *y2) +{ + *x1 = -G_MAXDOUBLE; + *y1 = -G_MAXDOUBLE; + *x2 = G_MAXDOUBLE; + *y2 = G_MAXDOUBLE; +} + +static void +ecb_update (GnomeCanvasItem *item, + const cairo_matrix_t *i2c, + gint flags) +{ + gdouble x1, y1, x2, y2; + + x1 = item->x1; + y1 = item->y1; + x2 = item->x2; + y2 = item->y2; + + /* The bounds are all constants so we should only have to + * redraw once. */ + ecb_bounds (item, &item->x1, &item->y1, &item->x2, &item->y2); + + if (item->x1 != x1 || item->y1 != y1 || + item->x2 != x2 || item->y2 != y2) + gnome_canvas_request_redraw ( + item->canvas, item->x1, item->y1, item->x2, item->y2); +} + +static void +ecb_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ECanvasBackground *ecb; + + GdkColor color = { 0, 0, 0, 0, }; + GdkColor *pcolor; + + ecb = E_CANVAS_BACKGROUND (object); + + switch (property_id) { + case PROP_FILL_COLOR: + if (g_value_get_string (value)) + gdk_color_parse (g_value_get_string (value), &color); + + ecb->priv->rgba = ((color.red & 0xff00) << 16 | + (color.green & 0xff00) << 8 | + (color.blue & 0xff00) | + 0xff); + break; + + case PROP_FILL_COLOR_GDK: + pcolor = g_value_get_boxed (value); + if (pcolor) { + color = *pcolor; + } + + ecb->priv->rgba = ((color.red & 0xff00) << 16 | + (color.green & 0xff00) << 8 | + (color.blue & 0xff00) | + 0xff); + break; + + case PROP_FILL_COLOR_RGBA: + ecb->priv->rgba = g_value_get_uint (value); + break; + + } + + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ecb)); +} + +static void +ecb_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ECanvasBackground *ecb; + + ecb = E_CANVAS_BACKGROUND (object); + + switch (property_id) { + case PROP_FILL_COLOR_RGBA: + g_value_set_uint (value, ecb->priv->rgba); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +ecb_init (ECanvasBackground *ecb) +{ + ecb->priv = E_CANVAS_BACKGROUND_GET_PRIVATE (ecb); +} + +static void +ecb_draw (GnomeCanvasItem *item, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height) +{ + ECanvasBackground *ecb = E_CANVAS_BACKGROUND (item); + + cairo_save (cr); + cairo_set_source_rgba ( + cr, + ((ecb->priv->rgba >> 24) & 0xff) / 255.0, + ((ecb->priv->rgba >> 16) & 0xff) / 255.0, + ((ecb->priv->rgba >> 8) & 0xff) / 255.0, + ( ecb->priv->rgba & 0xff) / 255.0); + cairo_paint (cr); + cairo_restore (cr); +} + +static GnomeCanvasItem * +ecb_point (GnomeCanvasItem *item, + gdouble x, + gdouble y, + gint cx, + gint cy) +{ + return item; +} + +static void +ecb_style_set (ECanvasBackground *ecb, + GtkStyle *previous_style) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (ecb); + + if (gtk_widget_get_realized (GTK_WIDGET (item->canvas))) + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ecb)); +} + +static void +ecb_class_init (ECanvasBackgroundClass *ecb_class) +{ + GnomeCanvasItemClass *item_class = GNOME_CANVAS_ITEM_CLASS (ecb_class); + GObjectClass *object_class = G_OBJECT_CLASS (ecb_class); + + g_type_class_add_private (ecb_class, sizeof (ECanvasBackgroundPrivate)); + + object_class->set_property = ecb_set_property; + object_class->get_property = ecb_get_property; + + item_class->update = ecb_update; + item_class->draw = ecb_draw; + item_class->point = ecb_point; + item_class->bounds = ecb_bounds; + + ecb_class->style_set = ecb_style_set; + + g_object_class_install_property ( + object_class, + PROP_FILL_COLOR, + g_param_spec_string ( + "fill_color", + "Fill color", + "Fill color", + NULL, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_FILL_COLOR_GDK, + g_param_spec_boxed ( + "fill_color_gdk", + "GDK fill color", + "GDK fill color", + GDK_TYPE_COLOR, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_FILL_COLOR_RGBA, + g_param_spec_uint ( + "fill_color_rgba", + "GDK fill color", + "GDK fill color", + 0, G_MAXUINT, 0, + G_PARAM_READWRITE)); + + ecb_signals[STYLE_SET] = g_signal_new ( + "style_set", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ECanvasBackgroundClass, style_set), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + GTK_TYPE_STYLE); +} + diff --git a/e-util/e-canvas-background.h b/e-util/e-canvas-background.h new file mode 100644 index 0000000000..c57c64f787 --- /dev/null +++ b/e-util/e-canvas-background.h @@ -0,0 +1,75 @@ +/* + * e-canvas-background.h - background color for canvas. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CANVAS_BACKGROUND_H +#define E_CANVAS_BACKGROUND_H + +#include <libgnomecanvas/gnome-canvas.h> + +/* Standard GObject macros */ +#define E_TYPE_CANVAS_BACKGROUND \ + (e_canvas_background_get_type ()) +#define E_CANVAS_BACKGROUND(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CANVAS_BACKGROUND, ECanvasBackground)) +#define E_CANVAS_BACKGROUND_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CANVAS_BACKGROUND, ECanvasBackgroundClass)) +#define E_IS_CANVAS_BACKGROUND(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CANVAS_BACKGROUND)) +#define E_IS_CANVAS_BACKGROUND_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CANVAS_BACKGROUND)) +#define E_CANVAS_BACKGROUND_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CANVAS_BACKGROUND, ECanvasBackgroundClass)) + +G_BEGIN_DECLS + +typedef struct _ECanvasBackground ECanvasBackground; +typedef struct _ECanvasBackgroundClass ECanvasBackgroundClass; +typedef struct _ECanvasBackgroundPrivate ECanvasBackgroundPrivate; + +struct _ECanvasBackground { + GnomeCanvasItem item; + ECanvasBackgroundPrivate *priv; +}; + +struct _ECanvasBackgroundClass { + GnomeCanvasItemClass parent_class; + + void (*style_set) (ECanvasBackground *eti, + GtkStyle *previous_style); +}; + +GType e_canvas_background_get_type (void) G_GNUC_CONST; + +G_END_DECLS + +#endif /* E_CANVAS_BACKGROUND */ diff --git a/e-util/e-canvas-utils.c b/e-util/e-canvas-utils.c new file mode 100644 index 0000000000..ec3aad3858 --- /dev/null +++ b/e-util/e-canvas-utils.c @@ -0,0 +1,222 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-canvas-utils.h" + +void +e_canvas_item_move_absolute (GnomeCanvasItem *item, + gdouble dx, + gdouble dy) +{ + cairo_matrix_t translate; + + g_return_if_fail (GNOME_IS_CANVAS_ITEM (item)); + + cairo_matrix_init_translate (&translate, dx, dy); + + gnome_canvas_item_set_matrix (item, &translate); +} + +static double +compute_offset (gint top, + gint bottom, + gint page_top, + gint page_bottom) +{ + gint size = bottom - top; + gint offset = 0; + + if (top <= page_top && bottom >= page_bottom) + return 0; + + if (bottom > page_bottom) + offset = (bottom - page_bottom); + if (top < page_top + offset) + offset = (top - page_top); + + if (top <= page_top + offset && bottom >= page_bottom + offset) + return offset; + + if (top < page_top + size * 3 / 2 + offset) + offset = top - (page_top + size * 3 / 2); + if (bottom > page_bottom - size * 3 / 2 + offset) + offset = bottom - (page_bottom - size * 3 / 2); + if (top < page_top + size * 3 / 2 + offset) + offset = top - ((page_top + page_bottom - (bottom - top)) / 2); + + return offset; +} + +static void +e_canvas_show_area (GnomeCanvas *canvas, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2) +{ + GtkAdjustment *h, *v; + gint dx = 0, dy = 0; + gdouble page_size; + gdouble lower; + gdouble upper; + gdouble value; + + g_return_if_fail (canvas != NULL); + g_return_if_fail (GNOME_IS_CANVAS (canvas)); + + h = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (canvas)); + page_size = gtk_adjustment_get_page_size (h); + lower = gtk_adjustment_get_lower (h); + upper = gtk_adjustment_get_upper (h); + value = gtk_adjustment_get_value (h); + dx = compute_offset (x1, x2, value, value + page_size); + if (dx) + gtk_adjustment_set_value (h, CLAMP (value + dx, lower, upper - page_size)); + + v = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (canvas)); + page_size = gtk_adjustment_get_page_size (v); + lower = gtk_adjustment_get_lower (v); + upper = gtk_adjustment_get_upper (v); + value = gtk_adjustment_get_value (v); + dy = compute_offset (y1, y2, value, value + page_size); + if (dy) + gtk_adjustment_set_value (v, CLAMP (value + dy, lower, upper - page_size)); +} + +void +e_canvas_item_show_area (GnomeCanvasItem *item, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2) +{ + g_return_if_fail (item != NULL); + g_return_if_fail (GNOME_IS_CANVAS_ITEM (item)); + + gnome_canvas_item_i2w (item, &x1, &y1); + gnome_canvas_item_i2w (item, &x2, &y2); + + e_canvas_show_area (item->canvas, x1, y1, x2, y2); +} + +static gboolean +e_canvas_area_shown (GnomeCanvas *canvas, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2) +{ + GtkAdjustment *h, *v; + gint dx = 0, dy = 0; + gdouble page_size; + gdouble lower; + gdouble upper; + gdouble value; + + g_return_val_if_fail (canvas != NULL, FALSE); + g_return_val_if_fail (GNOME_IS_CANVAS (canvas), FALSE); + + h = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (canvas)); + page_size = gtk_adjustment_get_page_size (h); + lower = gtk_adjustment_get_lower (h); + upper = gtk_adjustment_get_upper (h); + value = gtk_adjustment_get_value (h); + dx = compute_offset (x1, x2, value, value + page_size); + if (CLAMP (value + dx, lower, upper - page_size) - value != 0) + return FALSE; + + v = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (canvas)); + page_size = gtk_adjustment_get_page_size (v); + lower = gtk_adjustment_get_lower (v); + upper = gtk_adjustment_get_upper (v); + value = gtk_adjustment_get_value (v); + dy = compute_offset (y1, y2, value, value + page_size); + if (CLAMP (value + dy, lower, upper - page_size) - value != 0) + return FALSE; + return TRUE; +} + +gboolean +e_canvas_item_area_shown (GnomeCanvasItem *item, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2) +{ + g_return_val_if_fail (item != NULL, FALSE); + g_return_val_if_fail (GNOME_IS_CANVAS_ITEM (item), FALSE); + + gnome_canvas_item_i2w (item, &x1, &y1); + gnome_canvas_item_i2w (item, &x2, &y2); + + return e_canvas_area_shown (item->canvas, x1, y1, x2, y2); +} + +typedef struct { + gdouble x1; + gdouble y1; + gdouble x2; + gdouble y2; + GnomeCanvas *canvas; +} DoubsAndCanvas; + +static gboolean +show_area_timeout (gpointer data) +{ + DoubsAndCanvas *dac = data; + + e_canvas_show_area (dac->canvas, dac->x1, dac->y1, dac->x2, dac->y2); + g_object_unref (dac->canvas); + g_free (dac); + return FALSE; +} + +void +e_canvas_item_show_area_delayed (GnomeCanvasItem *item, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2, + gint delay) +{ + DoubsAndCanvas *dac; + + g_return_if_fail (item != NULL); + g_return_if_fail (GNOME_IS_CANVAS_ITEM (item)); + + gnome_canvas_item_i2w (item, &x1, &y1); + gnome_canvas_item_i2w (item, &x2, &y2); + + dac = g_new (DoubsAndCanvas, 1); + dac->x1 = x1; + dac->y1 = y1; + dac->x2 = x2; + dac->y2 = y2; + dac->canvas = item->canvas; + g_object_ref (item->canvas); + g_timeout_add (delay, show_area_timeout, dac); +} diff --git a/e-util/e-canvas-utils.h b/e-util/e-canvas-utils.h new file mode 100644 index 0000000000..c12f156c59 --- /dev/null +++ b/e-util/e-canvas-utils.h @@ -0,0 +1,59 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __E_CANVAS_UTILS__ +#define __E_CANVAS_UTILS__ + +#include <libgnomecanvas/gnome-canvas.h> + +G_BEGIN_DECLS + +void e_canvas_item_move_absolute (GnomeCanvasItem *item, + gdouble dx, + gdouble dy); +void e_canvas_item_show_area (GnomeCanvasItem *item, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2); +void e_canvas_item_show_area_delayed (GnomeCanvasItem *item, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2, + gint delay); +/* Returns TRUE if the area is already shown on the screen (including + * spacing.) This is equivelent to returning FALSE iff show_area + * would do anything. */ +gboolean e_canvas_item_area_shown (GnomeCanvasItem *item, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2); + +G_END_DECLS + +#endif /* __E_CANVAS_UTILS__ */ diff --git a/e-util/e-canvas-vbox.c b/e-util/e-canvas-vbox.c new file mode 100644 index 0000000000..f8de013557 --- /dev/null +++ b/e-util/e-canvas-vbox.c @@ -0,0 +1,410 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <math.h> + +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> + +#include <glib/gi18n.h> + +#include "e-canvas.h" +#include "e-canvas-utils.h" +#include "e-canvas-vbox.h" + +static void e_canvas_vbox_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); +static void e_canvas_vbox_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); +static void e_canvas_vbox_dispose (GObject *object); + +static gint e_canvas_vbox_event (GnomeCanvasItem *item, GdkEvent *event); +static void e_canvas_vbox_realize (GnomeCanvasItem *item); + +static void e_canvas_vbox_reflow (GnomeCanvasItem *item, gint flags); + +static void e_canvas_vbox_real_add_item (ECanvasVbox *e_canvas_vbox, GnomeCanvasItem *item); +static void e_canvas_vbox_real_add_item_start (ECanvasVbox *e_canvas_vbox, GnomeCanvasItem *item); +static void e_canvas_vbox_resize_children (GnomeCanvasItem *item); + +enum { + PROP_0, + PROP_WIDTH, + PROP_MINIMUM_WIDTH, + PROP_HEIGHT, + PROP_SPACING +}; + +G_DEFINE_TYPE ( + ECanvasVbox, + e_canvas_vbox, + GNOME_TYPE_CANVAS_GROUP) + +static void +e_canvas_vbox_class_init (ECanvasVboxClass *class) +{ + GObjectClass *object_class; + GnomeCanvasItemClass *item_class; + + object_class = (GObjectClass *) class; + item_class = (GnomeCanvasItemClass *) class; + + class->add_item = e_canvas_vbox_real_add_item; + class->add_item_start = e_canvas_vbox_real_add_item_start; + + object_class->set_property = e_canvas_vbox_set_property; + object_class->get_property = e_canvas_vbox_get_property; + object_class->dispose = e_canvas_vbox_dispose; + + /* GnomeCanvasItem method overrides */ + item_class->event = e_canvas_vbox_event; + item_class->realize = e_canvas_vbox_realize; + + g_object_class_install_property ( + object_class, + PROP_WIDTH, + g_param_spec_double ( + "width", + "Width", + "Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + g_object_class_install_property ( + object_class, + PROP_MINIMUM_WIDTH, + g_param_spec_double ( + "minimum_width", + "Minimum width", + "Minimum Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + g_object_class_install_property ( + object_class, + PROP_HEIGHT, + g_param_spec_double ( + "height", + "Height", + "Height", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READABLE)); + g_object_class_install_property ( + object_class, + PROP_SPACING, + g_param_spec_double ( + "spacing", + "Spacing", + "Spacing", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); +} + +static void +e_canvas_vbox_init (ECanvasVbox *vbox) +{ + vbox->items = NULL; + + vbox->width = 10; + vbox->minimum_width = 10; + vbox->height = 10; + vbox->spacing = 0; + + e_canvas_item_set_reflow_callback (GNOME_CANVAS_ITEM (vbox), e_canvas_vbox_reflow); +} + +static void +e_canvas_vbox_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GnomeCanvasItem *item; + ECanvasVbox *e_canvas_vbox; + + item = GNOME_CANVAS_ITEM (object); + e_canvas_vbox = E_CANVAS_VBOX (object); + + switch (property_id) { + case PROP_WIDTH: + case PROP_MINIMUM_WIDTH: + e_canvas_vbox->minimum_width = g_value_get_double (value); + e_canvas_vbox_resize_children (item); + e_canvas_item_request_reflow (item); + break; + case PROP_SPACING: + e_canvas_vbox->spacing = g_value_get_double (value); + e_canvas_item_request_reflow (item); + break; + } +} + +static void +e_canvas_vbox_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ECanvasVbox *e_canvas_vbox; + + e_canvas_vbox = E_CANVAS_VBOX (object); + + switch (property_id) { + case PROP_WIDTH: + g_value_set_double (value, e_canvas_vbox->width); + break; + case PROP_MINIMUM_WIDTH: + g_value_set_double (value, e_canvas_vbox->minimum_width); + break; + case PROP_HEIGHT: + g_value_set_double (value, e_canvas_vbox->height); + break; + case PROP_SPACING: + g_value_set_double (value, e_canvas_vbox->spacing); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +/* Used from g_list_foreach(); disconnects from an item's signals */ +static void +disconnect_item_cb (gpointer data, + gpointer user_data) +{ + ECanvasVbox *vbox; + GnomeCanvasItem *item; + + vbox = E_CANVAS_VBOX (user_data); + + item = GNOME_CANVAS_ITEM (data); + g_signal_handlers_disconnect_matched ( + item, + G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, + vbox); +} + +static void +e_canvas_vbox_dispose (GObject *object) +{ + ECanvasVbox *vbox = E_CANVAS_VBOX (object); + + if (vbox->items) { + g_list_foreach (vbox->items, disconnect_item_cb, vbox); + g_list_free (vbox->items); + vbox->items = NULL; + } + + G_OBJECT_CLASS (e_canvas_vbox_parent_class)->dispose (object); +} + +static gint +e_canvas_vbox_event (GnomeCanvasItem *item, + GdkEvent *event) +{ + gint return_val = TRUE; + + switch (event->type) { + case GDK_KEY_PRESS: + switch (event->key.keyval) { + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + return_val = TRUE; + break; + default: + return_val = FALSE; + break; + } + break; + default: + return_val = FALSE; + break; + } + if (!return_val) { + if (GNOME_CANVAS_ITEM_CLASS (e_canvas_vbox_parent_class)->event) + return GNOME_CANVAS_ITEM_CLASS (e_canvas_vbox_parent_class)->event (item, event); + } + return return_val; + +} + +static void +e_canvas_vbox_realize (GnomeCanvasItem *item) +{ + if (GNOME_CANVAS_ITEM_CLASS (e_canvas_vbox_parent_class)->realize) + (* GNOME_CANVAS_ITEM_CLASS (e_canvas_vbox_parent_class)->realize) (item); + + e_canvas_vbox_resize_children (item); + e_canvas_item_request_reflow (item); +} + +static void +e_canvas_vbox_remove_item (gpointer data, + GObject *where_object_was) +{ + ECanvasVbox *vbox = data; + vbox->items = g_list_remove (vbox->items, where_object_was); +} + +static void +e_canvas_vbox_real_add_item (ECanvasVbox *e_canvas_vbox, + GnomeCanvasItem *item) +{ + e_canvas_vbox->items = g_list_append (e_canvas_vbox->items, item); + g_object_weak_ref ( + G_OBJECT (item), + e_canvas_vbox_remove_item, e_canvas_vbox); + if (GNOME_CANVAS_ITEM (e_canvas_vbox)->flags & GNOME_CANVAS_ITEM_REALIZED) { + gnome_canvas_item_set ( + item, + "width", (gdouble) e_canvas_vbox->minimum_width, + NULL); + e_canvas_item_request_reflow (item); + } +} + +static void +e_canvas_vbox_real_add_item_start (ECanvasVbox *e_canvas_vbox, + GnomeCanvasItem *item) +{ + e_canvas_vbox->items = g_list_prepend (e_canvas_vbox->items, item); + g_object_weak_ref ( + G_OBJECT (item), + e_canvas_vbox_remove_item, e_canvas_vbox); + if (GNOME_CANVAS_ITEM (e_canvas_vbox)->flags & GNOME_CANVAS_ITEM_REALIZED) { + gnome_canvas_item_set ( + item, + "width", (gdouble) e_canvas_vbox->minimum_width, + NULL); + e_canvas_item_request_reflow (item); + } +} + +static void +e_canvas_vbox_resize_children (GnomeCanvasItem *item) +{ + GList *list; + ECanvasVbox *e_canvas_vbox; + + e_canvas_vbox = E_CANVAS_VBOX (item); + for (list = e_canvas_vbox->items; list; list = list->next) { + GnomeCanvasItem *child = GNOME_CANVAS_ITEM (list->data); + gnome_canvas_item_set ( + child, + "width", (gdouble) e_canvas_vbox->minimum_width, + NULL); + } +} + +static void +e_canvas_vbox_reflow (GnomeCanvasItem *item, + gint flags) +{ + ECanvasVbox *e_canvas_vbox = E_CANVAS_VBOX (item); + if (item->flags & GNOME_CANVAS_ITEM_REALIZED) { + + gdouble old_height; + gdouble running_height; + gdouble old_width; + gdouble max_width; + + old_width = e_canvas_vbox->width; + max_width = e_canvas_vbox->minimum_width; + + old_height = e_canvas_vbox->height; + running_height = 0; + + if (e_canvas_vbox->items == NULL) { + } else { + GList *list; + gdouble item_height; + gdouble item_width; + + list = e_canvas_vbox->items; + g_object_get ( + list->data, + "height", &item_height, + "width", &item_width, + NULL); + e_canvas_item_move_absolute ( + GNOME_CANVAS_ITEM (list->data), + (gdouble) 0, + (gdouble) running_height); + running_height += item_height; + if (max_width < item_width) + max_width = item_width; + list = g_list_next (list); + + for (; list; list = g_list_next (list)) { + running_height += e_canvas_vbox->spacing; + + g_object_get ( + list->data, + "height", &item_height, + "width", &item_width, + NULL); + + e_canvas_item_move_absolute ( + GNOME_CANVAS_ITEM (list->data), + (gdouble) 0, + (gdouble) running_height); + + running_height += item_height; + if (max_width < item_width) + max_width = item_width; + } + + } + e_canvas_vbox->height = running_height; + e_canvas_vbox->width = max_width; + if (old_height != e_canvas_vbox->height || + old_width != e_canvas_vbox->width) + e_canvas_item_request_parent_reflow (item); + } +} + +void +e_canvas_vbox_add_item (ECanvasVbox *e_canvas_vbox, + GnomeCanvasItem *item) +{ + if (E_CANVAS_VBOX_CLASS (G_OBJECT_GET_CLASS (e_canvas_vbox))->add_item) + (E_CANVAS_VBOX_CLASS (G_OBJECT_GET_CLASS (e_canvas_vbox))->add_item) (e_canvas_vbox, item); +} + +void +e_canvas_vbox_add_item_start (ECanvasVbox *e_canvas_vbox, + GnomeCanvasItem *item) +{ + if (E_CANVAS_VBOX_CLASS (G_OBJECT_GET_CLASS (e_canvas_vbox))->add_item_start) + (E_CANVAS_VBOX_CLASS (G_OBJECT_GET_CLASS (e_canvas_vbox))->add_item_start) (e_canvas_vbox, item); +} + diff --git a/e-util/e-canvas-vbox.h b/e-util/e-canvas-vbox.h new file mode 100644 index 0000000000..5255e7683d --- /dev/null +++ b/e-util/e-canvas-vbox.h @@ -0,0 +1,92 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CANVAS_VBOX_H +#define E_CANVAS_VBOX_H + +#include <gtk/gtk.h> +#include <libgnomecanvas/gnome-canvas.h> + +/* Standard GObject macros */ +#define E_TYPE_CANVAS_VBOX \ + (e_canvas_vbox_get_type ()) +#define E_CANVAS_VBOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CANVAS_VBOX, ECanvasVbox)) +#define E_CANVAS_VBOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CANVAS_VBOX, ECanvasVboxClass)) +#define E_IS_CANVAS_VBOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CANVAS_VBOX)) +#define E_IS_CANVAS_VBOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CANVAS_VBOX)) +#define E_CANVAS_VBOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CANVAS_VBOX, ECanvasVboxClass)) + +G_BEGIN_DECLS + +typedef struct _ECanvasVbox ECanvasVbox; +typedef struct _ECanvasVboxClass ECanvasVboxClass; + +struct _ECanvasVbox { + GnomeCanvasGroup parent; + + /* item specific fields */ + GList *items; /* Of type GnomeCanvasItem */ + + gdouble width; + gdouble minimum_width; + gdouble height; + gdouble spacing; +}; + +struct _ECanvasVboxClass { + GnomeCanvasGroupClass parent_class; + + void (*add_item) (ECanvasVbox *canvas_vbox, + GnomeCanvasItem *item); + void (*add_item_start) (ECanvasVbox *canvas_vbox, + GnomeCanvasItem *item); +}; + +/* + * To be added to a CanvasVbox, an item must have the argument "width" as + * a Read/Write argument and "height" as a Read Only argument. It + * should also do an ECanvas parent CanvasVbox request if its size + * changes. + */ +GType e_canvas_vbox_get_type (void) G_GNUC_CONST; +void e_canvas_vbox_add_item (ECanvasVbox *canvas_vbox, + GnomeCanvasItem *item); +void e_canvas_vbox_add_item_start (ECanvasVbox *canvas_vbox, + GnomeCanvasItem *item); + +G_END_DECLS + +#endif /* E_CANVAS_VBOX_H */ diff --git a/e-util/e-canvas.c b/e-util/e-canvas.c new file mode 100644 index 0000000000..d39f9f7684 --- /dev/null +++ b/e-util/e-canvas.c @@ -0,0 +1,880 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> + +#include "e-canvas.h" + +#define d(x) + +enum { + REFLOW, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE ( + ECanvas, + e_canvas, + GNOME_TYPE_CANVAS) + +/* Emits an event for an item in the canvas, be it the current + * item, grabbed item, or focused item, as appropriate. */ +static gint +canvas_emit_event (GnomeCanvas *canvas, + GdkEvent *event) +{ + GdkEvent *ev; + gint finished; + GnomeCanvasItem *item; + GnomeCanvasItem *parent; + guint mask; + + /* Choose where we send the event */ + + item = canvas->current_item; + + if (canvas->focused_item && + ((event->type == GDK_KEY_PRESS) || + (event->type == GDK_KEY_RELEASE) || + (event->type == GDK_FOCUS_CHANGE))) + item = canvas->focused_item; + + if (canvas->grabbed_item) + item = canvas->grabbed_item; + + /* Perform checks for grabbed items */ + + if (canvas->grabbed_item) { + switch (event->type) { + case GDK_ENTER_NOTIFY: + mask = GDK_ENTER_NOTIFY_MASK; + break; + + case GDK_LEAVE_NOTIFY: + mask = GDK_LEAVE_NOTIFY_MASK; + break; + + case GDK_MOTION_NOTIFY: + mask = GDK_POINTER_MOTION_MASK; + break; + + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + mask = GDK_BUTTON_PRESS_MASK; + break; + + case GDK_BUTTON_RELEASE: + mask = GDK_BUTTON_RELEASE_MASK; + break; + + case GDK_KEY_PRESS: + mask = GDK_KEY_PRESS_MASK; + break; + + case GDK_KEY_RELEASE: + mask = GDK_KEY_RELEASE_MASK; + break; + + default: + mask = 0; + break; + } + + if (!(mask & canvas->grabbed_event_mask)) + return FALSE; + } + + /* Convert to world coordinates -- we have two cases because of + * different offsets of the fields in the event structures. */ + + ev = gdk_event_copy (event); + + switch (ev->type) { + case GDK_ENTER_NOTIFY: + case GDK_LEAVE_NOTIFY: + gnome_canvas_window_to_world ( + canvas, + ev->crossing.x, ev->crossing.y, + &ev->crossing.x, &ev->crossing.y); + break; + + case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + gnome_canvas_window_to_world ( + canvas, + ev->motion.x, ev->motion.y, + &ev->motion.x, &ev->motion.y); + break; + + default: + break; + } + + /* The event is propagated up the hierarchy (for if someone connected + * to a group instead of a leaf event), and emission is stopped if a + * handler returns TRUE, just like for GtkWidget events. */ + + finished = FALSE; + + while (item && !finished) { + g_object_ref (item); + + g_signal_emit_by_name (item, "event", ev, &finished); + + parent = item->parent; + g_object_unref (item); + + item = parent; + } + + gdk_event_free (ev); + + return finished; +} + +/* This routine invokes the point method of the item. The argument x, y + * should be in the parent's item-relative coordinate system. This routine + * applies the inverse of the item's transform, maintaining the affine + * invariant. */ +static GnomeCanvasItem * +gnome_canvas_item_invoke_point (GnomeCanvasItem *item, + gdouble x, + gdouble y, + gint cx, + gint cy) +{ + cairo_matrix_t inverse; + + /* Calculate x & y in item local coordinates */ + inverse = item->matrix; + if (cairo_matrix_invert (&inverse) != CAIRO_STATUS_SUCCESS) + return NULL; + + cairo_matrix_transform_point (&inverse, &x, &y); + + if (GNOME_CANVAS_ITEM_GET_CLASS (item)->point) + return GNOME_CANVAS_ITEM_GET_CLASS (item)->point (item, x, y, cx, cy); + + return NULL; +} + +/* Re-picks the current item in the canvas, based on the event's coordinates. + * Also emits enter/leave events for items as appropriate. + */ +#define DISPLAY_X1(canvas) (GNOME_CANVAS (canvas)->layout.xoffset) +#define DISPLAY_Y1(canvas) (GNOME_CANVAS (canvas)->layout.yoffset) +static gint +pick_current_item (GnomeCanvas *canvas, + GdkEvent *event) +{ + gint button_down; + gdouble x, y; + gint cx, cy; + gint retval; + + retval = FALSE; + + /* If a button is down, we'll perform enter and leave events on the + * current item, but not enter on any other item. This is more or less + * like X pointer grabbing for canvas items. + */ + button_down = canvas->state & (GDK_BUTTON1_MASK + | GDK_BUTTON2_MASK + | GDK_BUTTON3_MASK + | GDK_BUTTON4_MASK + | GDK_BUTTON5_MASK); + if (!button_down) + canvas->left_grabbed_item = FALSE; + + /* Save the event in the canvas. This is used to synthesize enter and + * leave events in case the current item changes. It is also used to + * re-pick the current item if the current one gets deleted. Also, + * synthesize an enter event. + */ + if (event != &canvas->pick_event) { + if ((event->type == GDK_MOTION_NOTIFY) || + (event->type == GDK_BUTTON_RELEASE)) { + /* these fields have the same offsets in both types of events */ + + canvas->pick_event.crossing.type = GDK_ENTER_NOTIFY; + canvas->pick_event.crossing.window = event->motion.window; + canvas->pick_event.crossing.send_event = event->motion.send_event; + canvas->pick_event.crossing.subwindow = NULL; + canvas->pick_event.crossing.x = event->motion.x; + canvas->pick_event.crossing.y = event->motion.y; + canvas->pick_event.crossing.mode = GDK_CROSSING_NORMAL; + canvas->pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR; + canvas->pick_event.crossing.focus = FALSE; + canvas->pick_event.crossing.state = event->motion.state; + + /* these fields don't have the same offsets in both types of events */ + + if (event->type == GDK_MOTION_NOTIFY) { + canvas->pick_event.crossing.x_root = event->motion.x_root; + canvas->pick_event.crossing.y_root = event->motion.y_root; + } else { + canvas->pick_event.crossing.x_root = event->button.x_root; + canvas->pick_event.crossing.y_root = event->button.y_root; + } + } else + canvas->pick_event = *event; + } + + /* Don't do anything else if this is a recursive call */ + + if (canvas->in_repick) + return retval; + + /* LeaveNotify means that there is no current item, so we don't look for one */ + + if (canvas->pick_event.type != GDK_LEAVE_NOTIFY) { + /* these fields don't have the same offsets in both types of events */ + + if (canvas->pick_event.type == GDK_ENTER_NOTIFY) { + x = canvas->pick_event.crossing.x + + canvas->scroll_x1 - canvas->zoom_xofs; + y = canvas->pick_event.crossing.y + + canvas->scroll_y1 - canvas->zoom_yofs; + } else { + x = canvas->pick_event.motion.x + + canvas->scroll_x1 - canvas->zoom_xofs; + y = canvas->pick_event.motion.y + + canvas->scroll_y1 - canvas->zoom_yofs; + } + + /* canvas pixel coords */ + + cx = (gint) (x + 0.5); + cy = (gint) (y + 0.5); + + /* world coords */ + + x = canvas->scroll_x1 + x; + y = canvas->scroll_y1 + y; + + /* find the closest item */ + + if (canvas->root->flags & GNOME_CANVAS_ITEM_VISIBLE) + canvas->new_current_item = + gnome_canvas_item_invoke_point ( + canvas->root, x, y, cx, cy); + else + canvas->new_current_item = NULL; + } else + canvas->new_current_item = NULL; + + if ((canvas->new_current_item == canvas->current_item) && + !canvas->left_grabbed_item) + return retval; /* current item did not change */ + + /* Synthesize events for old and new current items */ + + if ((canvas->new_current_item != canvas->current_item) + && (canvas->current_item != NULL) + && !canvas->left_grabbed_item) { + GdkEvent new_event = { 0 }; + + new_event = canvas->pick_event; + new_event.type = GDK_LEAVE_NOTIFY; + + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = NULL; + canvas->in_repick = TRUE; + retval = canvas_emit_event (canvas, &new_event); + canvas->in_repick = FALSE; + } + + /* new_current_item may have been set to NULL during + * the call to canvas_emit_event() above. */ + + if ((canvas->new_current_item != canvas->current_item) && button_down) { + canvas->left_grabbed_item = TRUE; + return retval; + } + + /* Handle the rest of cases */ + + canvas->left_grabbed_item = FALSE; + canvas->current_item = canvas->new_current_item; + + if (canvas->current_item != NULL) { + GdkEvent new_event = { 0 }; + + new_event = canvas->pick_event; + new_event.type = GDK_ENTER_NOTIFY; + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = NULL; + retval = canvas_emit_event (canvas, &new_event); + } + + return retval; +} + +static void +canvas_style_set_recursive (GnomeCanvasItem *item, + GtkStyle *previous_style) +{ + guint signal_id = g_signal_lookup ("style_set", G_OBJECT_TYPE (item)); + if (signal_id >= 1) { + GSignalQuery query; + g_signal_query (signal_id, &query); + if (query.return_type == G_TYPE_NONE && + query.n_params == 1 && + query.param_types[0] == GTK_TYPE_STYLE) { + g_signal_emit (item, signal_id, 0, previous_style); + } + } + + if (GNOME_IS_CANVAS_GROUP (item)) { + GList *items = GNOME_CANVAS_GROUP (item)->item_list; + for (; items; items = items->next) + canvas_style_set_recursive ( + items->data, previous_style); + } +} + +static void +canvas_dispose (GObject *object) +{ + ECanvas *canvas = E_CANVAS (object); + + if (canvas->idle_id) + g_source_remove (canvas->idle_id); + canvas->idle_id = 0; + + if (canvas->grab_cancelled_check_id) + g_source_remove (canvas->grab_cancelled_check_id); + canvas->grab_cancelled_check_id = 0; + + if (canvas->toplevel) { + if (canvas->visibility_notify_id) + g_signal_handler_disconnect ( + canvas->toplevel, + canvas->visibility_notify_id); + canvas->visibility_notify_id = 0; + + g_object_unref (canvas->toplevel); + canvas->toplevel = NULL; + } + + if (canvas->im_context) { + g_object_unref (canvas->im_context); + canvas->im_context = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_canvas_parent_class)->dispose (object); +} + +static void +canvas_realize (GtkWidget *widget) +{ + ECanvas *ecanvas = E_CANVAS (widget); + GdkWindow *window; + + /* Chain up to parent's realize() method. */ + GTK_WIDGET_CLASS (e_canvas_parent_class)->realize (widget); + + window = gtk_layout_get_bin_window (GTK_LAYOUT (widget)); + gdk_window_set_background_pattern (window, NULL); + + window = gtk_widget_get_window (widget); + gtk_im_context_set_client_window (ecanvas->im_context, window); +} + +static void +canvas_unrealize (GtkWidget *widget) +{ + ECanvas * ecanvas = E_CANVAS (widget); + + if (ecanvas->idle_id) { + g_source_remove (ecanvas->idle_id); + ecanvas->idle_id = 0; + } + + gtk_im_context_set_client_window (ecanvas->im_context, NULL); + + /* Chain up to parent's unrealize() method. */ + GTK_WIDGET_CLASS (e_canvas_parent_class)->unrealize (widget); +} + +static void +canvas_style_set (GtkWidget *widget, + GtkStyle *previous_style) +{ + canvas_style_set_recursive ( + GNOME_CANVAS_ITEM (gnome_canvas_root ( + GNOME_CANVAS (widget))), previous_style); +} + +static gint +canvas_button_event (GtkWidget *widget, + GdkEventButton *event) +{ + GnomeCanvas *canvas; + GdkWindow *bin_window; + gint mask; + gint retval; + + g_return_val_if_fail (GNOME_IS_CANVAS (widget), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + retval = FALSE; + + canvas = GNOME_CANVAS (widget); + bin_window = gtk_layout_get_bin_window (GTK_LAYOUT (canvas)); + + d ( + g_print ("button %d, event type %d, grabbed=%p, current=%p\n", + event->button, + event->type, + canvas->grabbed_item, + canvas->current_item)); + + /* dispatch normally regardless of the event's window if an item has + has a pointer grab in effect */ + if (!canvas->grabbed_item && event->window != bin_window) + return retval; + + switch (event->button) { + case 1: + mask = GDK_BUTTON1_MASK; + break; + case 2: + mask = GDK_BUTTON2_MASK; + break; + case 3: + mask = GDK_BUTTON3_MASK; + break; + case 4: + mask = GDK_BUTTON4_MASK; + break; + case 5: + mask = GDK_BUTTON5_MASK; + break; + default: + mask = 0; + } + + switch (event->type) { + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + /* Pick the current item as if the button were not + * pressed, and then process the event. */ + canvas->state = event->state; + pick_current_item (canvas, (GdkEvent *) event); + canvas->state ^= mask; + retval = canvas_emit_event (canvas, (GdkEvent *) event); + break; + + case GDK_BUTTON_RELEASE: + /* Process the event as if the button were pressed, + * then repick after the button has been released. */ + canvas->state = event->state; + retval = canvas_emit_event (canvas, (GdkEvent *) event); + event->state ^= mask; + canvas->state = event->state; + pick_current_item (canvas, (GdkEvent *) event); + event->state ^= mask; + break; + + default: + g_return_val_if_reached (0); + } + + return retval; +} + +static gint +canvas_key_event (GtkWidget *widget, + GdkEventKey *event) +{ + GnomeCanvas *canvas; + GdkEvent full_event = { 0 }; + + g_return_val_if_fail (GNOME_IS_CANVAS (widget), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + canvas = GNOME_CANVAS (widget); + + full_event.type = event->type; + full_event.key = *event; + + return canvas_emit_event (canvas, &full_event); +} + +static gint +canvas_focus_in_event (GtkWidget *widget, + GdkEventFocus *event) +{ + GnomeCanvas *canvas; + ECanvas *ecanvas; + GdkEvent full_event = { 0 }; + + canvas = GNOME_CANVAS (widget); + ecanvas = E_CANVAS (widget); + + /* XXX Can't access flags directly anymore, but is it really needed? + * If so, could we call gtk_widget_send_focus_change() instead? */ +#if 0 + GTK_WIDGET_SET_FLAGS (widget, GTK_HAS_FOCUS); +#endif + + gtk_im_context_focus_in (ecanvas->im_context); + + if (canvas->focused_item) { + full_event.type = event->type; + full_event.focus_change = *event; + return canvas_emit_event (canvas, &full_event); + } else { + return FALSE; + } +} + +static gint +canvas_focus_out_event (GtkWidget *widget, + GdkEventFocus *event) +{ + GnomeCanvas *canvas; + ECanvas *ecanvas; + GdkEvent full_event = { 0 }; + + canvas = GNOME_CANVAS (widget); + ecanvas = E_CANVAS (widget); + + /* XXX Can't access flags directly anymore, but is it really needed? + * If so, could we call gtk_widget_send_focus_change() instead? */ +#if 0 + GTK_WIDGET_UNSET_FLAGS (widget, GTK_HAS_FOCUS); +#endif + + gtk_im_context_focus_out (ecanvas->im_context); + + if (canvas->focused_item) { + full_event.type = event->type; + full_event.focus_change = *event; + return canvas_emit_event (canvas, &full_event); + } else { + return FALSE; + } +} + +static void +canvas_reflow (ECanvas *canvas) +{ + /* Placeholder so subclasses can safely chain up. */ +} + +static void +e_canvas_class_init (ECanvasClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = canvas_dispose; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->realize = canvas_realize; + widget_class->unrealize = canvas_unrealize; + widget_class->style_set = canvas_style_set; + widget_class->button_press_event = canvas_button_event; + widget_class->button_release_event = canvas_button_event; + widget_class->key_press_event = canvas_key_event; + widget_class->key_release_event = canvas_key_event; + widget_class->focus_in_event = canvas_focus_in_event; + widget_class->focus_out_event = canvas_focus_out_event; + + class->reflow = canvas_reflow; + + signals[REFLOW] = g_signal_new ( + "reflow", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ECanvasClass, reflow), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_canvas_init (ECanvas *canvas) +{ + canvas->im_context = gtk_im_multicontext_new (); +} + +GtkWidget * +e_canvas_new (void) +{ + return g_object_new (E_TYPE_CANVAS, NULL); +} + +/** + * e_canvas_item_grab_focus: + * @item: A canvas item. + * @widget_too: Whether or not to grab the widget-level focus too + * + * Makes the specified item take the keyboard focus, so all keyboard + * events will be sent to it. If the canvas widget itself did not have + * the focus and @widget_too is %TRUE, it grabs that focus as well. + **/ +void +e_canvas_item_grab_focus (GnomeCanvasItem *item, + gboolean widget_too) +{ + GnomeCanvasItem *focused_item; + GdkWindow *bin_window; + GdkEvent ev = { 0 }; + + g_return_if_fail (GNOME_IS_CANVAS_ITEM (item)); + g_return_if_fail (gtk_widget_get_can_focus (GTK_WIDGET (item->canvas))); + + bin_window = gtk_layout_get_bin_window (GTK_LAYOUT (item->canvas)); + + focused_item = item->canvas->focused_item; + + if (focused_item) { + ev.type = GDK_FOCUS_CHANGE; + ev.focus_change.type = GDK_FOCUS_CHANGE; + ev.focus_change.window = bin_window; + ev.focus_change.send_event = FALSE; + ev.focus_change.in = FALSE; + + canvas_emit_event (item->canvas, &ev); + } + + item->canvas->focused_item = item; + + if (widget_too && !gtk_widget_has_focus (GTK_WIDGET (item->canvas))) { + gtk_widget_grab_focus (GTK_WIDGET (item->canvas)); + } + + if (item) { + ev.focus_change.type = GDK_FOCUS_CHANGE; + ev.focus_change.window = bin_window; + ev.focus_change.send_event = FALSE; + ev.focus_change.in = TRUE; + + canvas_emit_event (item->canvas, &ev); + } +} + +static void +e_canvas_item_invoke_reflow (GnomeCanvasItem *item, + gint flags) +{ + GnomeCanvasGroup *group; + GList *list; + GnomeCanvasItem *child; + + if (GNOME_IS_CANVAS_GROUP (item)) { + group = GNOME_CANVAS_GROUP (item); + for (list = group->item_list; list; list = list->next) { + child = GNOME_CANVAS_ITEM (list->data); + if (child->flags & E_CANVAS_ITEM_DESCENDENT_NEEDS_REFLOW) + e_canvas_item_invoke_reflow (child, flags); + } + } + + if (item->flags & E_CANVAS_ITEM_NEEDS_REFLOW) { + ECanvasItemReflowFunc func; + func = (ECanvasItemReflowFunc) + g_object_get_data ( + G_OBJECT (item), + "ECanvasItem::reflow_callback"); + if (func) + func (item, flags); + } + + item->flags &= ~E_CANVAS_ITEM_NEEDS_REFLOW; + item->flags &= ~E_CANVAS_ITEM_DESCENDENT_NEEDS_REFLOW; +} + +static void +do_reflow (ECanvas *canvas) +{ + if (GNOME_CANVAS (canvas)->root->flags & E_CANVAS_ITEM_DESCENDENT_NEEDS_REFLOW) + e_canvas_item_invoke_reflow (GNOME_CANVAS (canvas)->root, 0); +} + +/* Idle handler for the e-canvas. It deals with pending reflows. */ +static gint +idle_handler (gpointer data) +{ + ECanvas *canvas; + + canvas = E_CANVAS (data); + do_reflow (canvas); + + /* Reset idle id */ + canvas->idle_id = 0; + + g_signal_emit (canvas, signals[REFLOW], 0); + + return FALSE; +} + +/* Convenience function to add an idle handler to a canvas */ +static void +add_idle (ECanvas *canvas) +{ + if (canvas->idle_id != 0) + return; + + canvas->idle_id = g_idle_add_full ( + G_PRIORITY_HIGH_IDLE, idle_handler, (gpointer) canvas, NULL); +} + +static void +e_canvas_item_descendent_needs_reflow (GnomeCanvasItem *item) +{ + if (item->flags & E_CANVAS_ITEM_DESCENDENT_NEEDS_REFLOW) + return; + + item->flags |= E_CANVAS_ITEM_DESCENDENT_NEEDS_REFLOW; + if (item->parent) + e_canvas_item_descendent_needs_reflow (item->parent); +} + +void +e_canvas_item_request_reflow (GnomeCanvasItem *item) +{ + g_return_if_fail (GNOME_IS_CANVAS_ITEM (item)); + + if (item->flags & GNOME_CANVAS_ITEM_REALIZED) { + item->flags |= E_CANVAS_ITEM_NEEDS_REFLOW; + e_canvas_item_descendent_needs_reflow (item); + add_idle (E_CANVAS (item->canvas)); + } +} + +void +e_canvas_item_request_parent_reflow (GnomeCanvasItem *item) +{ + g_return_if_fail (GNOME_IS_CANVAS_ITEM (item)); + + e_canvas_item_request_reflow (item->parent); +} + +void +e_canvas_item_set_reflow_callback (GnomeCanvasItem *item, + ECanvasItemReflowFunc func) +{ + g_return_if_fail (GNOME_IS_CANVAS_ITEM (item)); + g_return_if_fail (func != NULL); + + g_object_set_data ( + G_OBJECT (item), "ECanvasItem::reflow_callback", + (gpointer) func); +} + +static gboolean +grab_cancelled_check (gpointer data) +{ + ECanvas *canvas = data; + + if (GNOME_CANVAS (canvas)->grabbed_item == NULL) { + canvas->grab_cancelled_cb = NULL; + canvas->grab_cancelled_check_id = 0; + canvas->grab_cancelled_time = 0; + canvas->grab_cancelled_data = NULL; + return FALSE; + } + + if (gtk_grab_get_current ()) { + gnome_canvas_item_ungrab ( + GNOME_CANVAS (canvas)->grabbed_item, + canvas->grab_cancelled_time); + if (canvas->grab_cancelled_cb) + canvas->grab_cancelled_cb ( + canvas, GNOME_CANVAS (canvas)->grabbed_item, + canvas->grab_cancelled_data); + canvas->grab_cancelled_cb = NULL; + canvas->grab_cancelled_check_id = 0; + canvas->grab_cancelled_time = 0; + canvas->grab_cancelled_data = NULL; + return FALSE; + } + return TRUE; +} + +gint +e_canvas_item_grab (ECanvas *canvas, + GnomeCanvasItem *item, + guint event_mask, + GdkCursor *cursor, + GdkDevice *device, + guint32 etime, + ECanvasItemGrabCancelled cancelled_cb, + gpointer cancelled_data) +{ + GdkGrabStatus grab_status; + + g_return_val_if_fail (E_IS_CANVAS (canvas), -1); + g_return_val_if_fail (GNOME_IS_CANVAS_ITEM (item), -1); + g_return_val_if_fail (GDK_IS_DEVICE (device), -1); + + if (gtk_grab_get_current ()) + return GDK_GRAB_ALREADY_GRABBED; + + grab_status = gnome_canvas_item_grab ( + item, event_mask, cursor, device, etime); + if (grab_status == GDK_GRAB_SUCCESS) { + canvas->grab_cancelled_cb = cancelled_cb; + canvas->grab_cancelled_check_id = g_timeout_add_full ( + G_PRIORITY_LOW, 100, + grab_cancelled_check, canvas, NULL); + canvas->grab_cancelled_time = etime; + canvas->grab_cancelled_data = cancelled_data; + } + + return grab_status; +} + +void +e_canvas_item_ungrab (ECanvas *canvas, + GnomeCanvasItem *item, + guint32 etime) +{ + g_return_if_fail (E_IS_CANVAS (canvas)); + g_return_if_fail (GNOME_IS_CANVAS_ITEM (item)); + + if (canvas->grab_cancelled_check_id) { + g_source_remove (canvas->grab_cancelled_check_id); + canvas->grab_cancelled_cb = NULL; + canvas->grab_cancelled_check_id = 0; + canvas->grab_cancelled_time = 0; + canvas->grab_cancelled_data = NULL; + gnome_canvas_item_ungrab (item, etime); + } +} diff --git a/e-util/e-canvas.h b/e-util/e-canvas.h new file mode 100644 index 0000000000..8d704b963c --- /dev/null +++ b/e-util/e-canvas.h @@ -0,0 +1,141 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CANVAS_H +#define E_CANVAS_H + +#include <gtk/gtk.h> +#include <libgnomecanvas/gnome-canvas.h> + +/* ECanvas - A class derived from canvas for the purpose of adding + * evolution specific canvas hacks. */ + +/* Standard GObject macros */ +#define E_TYPE_CANVAS \ + (e_canvas_get_type ()) +#define E_CANVAS(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CANVAS, ECanvas)) +#define E_CANVAS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CANVAS, ECanvasClass)) +#define E_IS_CANVAS(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CANVAS)) +#define E_IS_CANVAS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CANVAS)) +#define E_CANVAS_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CANVAS, ECanvasClass)) + +G_BEGIN_DECLS + +typedef void (*ECanvasItemReflowFunc) (GnomeCanvasItem *item, + gint flags); + +typedef void (*ECanvasItemSelectionFunc) (GnomeCanvasItem *item, + gint flags, + gpointer user_data); +/* Returns the same as strcmp does. */ +typedef gint (*ECanvasItemSelectionCompareFunc) + (GnomeCanvasItem *item, + gpointer data1, + gpointer data2, + gint flags); + +typedef struct _ECanvas ECanvas; +typedef struct _ECanvasClass ECanvasClass; + +/* Object flags for items */ +enum { + E_CANVAS_ITEM_NEEDS_REFLOW = 1 << 13, + E_CANVAS_ITEM_DESCENDENT_NEEDS_REFLOW = 1 << 14 +}; + +typedef struct { + GnomeCanvasItem *item; + gpointer id; +} ECanvasSelectionInfo; + +typedef void (*ECanvasItemGrabCancelled) (ECanvas *canvas, + GnomeCanvasItem *item, + gpointer data); + +struct _ECanvas { + GnomeCanvas parent; + + gint idle_id; + GList *selection; + ECanvasSelectionInfo *cursor; + + GtkWidget *tooltip_window; + gint visibility_notify_id; + GtkWidget *toplevel; + + /* Input context for dead key support */ + GtkIMContext *im_context; + + ECanvasItemGrabCancelled grab_cancelled_cb; + guint grab_cancelled_check_id; + guint32 grab_cancelled_time; + gpointer grab_cancelled_data; +}; + +struct _ECanvasClass { + GnomeCanvasClass parent_class; + + void (*reflow) (ECanvas *canvas); +}; + +GType e_canvas_get_type (void); +GtkWidget * e_canvas_new (void); + +/* Used to send all of the keystroke events to a specific item as well as + * GDK_FOCUS_CHANGE events. */ +void e_canvas_item_grab_focus (GnomeCanvasItem *item, + gboolean widget_too); +void e_canvas_item_request_reflow (GnomeCanvasItem *item); +void e_canvas_item_request_parent_reflow + (GnomeCanvasItem *item); +void e_canvas_item_set_reflow_callback + (GnomeCanvasItem *item, + ECanvasItemReflowFunc func); +gint e_canvas_item_grab (ECanvas *canvas, + GnomeCanvasItem *item, + guint event_mask, + GdkCursor *cursor, + GdkDevice *device, + guint32 etime, + ECanvasItemGrabCancelled cancelled, + gpointer cancelled_data); +void e_canvas_item_ungrab (ECanvas *canvas, + GnomeCanvasItem *item, + guint32 etime); + +G_END_DECLS + +#endif /* E_CANVAS_H */ diff --git a/e-util/e-categories-config.c b/e-util/e-categories-config.c index 519fc5db32..611c7fa4af 100644 --- a/e-util/e-categories-config.c +++ b/e-util/e-categories-config.c @@ -30,9 +30,8 @@ #include <gtk/gtk.h> #include <glib/gi18n.h> -#include <libedataserverui/libedataserverui.h> - -#include "e-util/e-util.h" +#include "e-categories-dialog.h" +#include "e-misc-utils.h" static GHashTable *pixbufs_cache = NULL; diff --git a/e-util/e-categories-config.h b/e-util/e-categories-config.h index 82294ae6b9..f778e0116f 100644 --- a/e-util/e-categories-config.h +++ b/e-util/e-categories-config.h @@ -23,6 +23,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef __E_CATEGORIES_CONFIG_H__ #define __E_CATEGORIES_CONFIG_H__ diff --git a/e-util/e-categories-dialog.c b/e-util/e-categories-dialog.c new file mode 100644 index 0000000000..8f46b1041d --- /dev/null +++ b/e-util/e-categories-dialog.c @@ -0,0 +1,155 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n-lib.h> + +#include <libedataserver/libedataserver.h> + +#include "e-categories-dialog.h" +#include "e-categories-editor.h" +#include "e-categories-selector.h" +#include "e-category-completion.h" +#include "e-category-editor.h" + +#define E_CATEGORIES_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CATEGORIES_DIALOG, ECategoriesDialogPrivate)) + +G_DEFINE_TYPE (ECategoriesDialog, e_categories_dialog, GTK_TYPE_DIALOG) + +struct _ECategoriesDialogPrivate { + GtkWidget *categories_editor; +}; + +static void +entry_changed_cb (GtkEntry *entry, + ECategoriesDialog *dialog) +{ + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (dialog), GTK_RESPONSE_OK, TRUE); +} + +static void +e_categories_dialog_class_init (ECategoriesDialogClass *class) +{ + g_type_class_add_private (class, sizeof (ECategoriesDialogPrivate)); +} + +static void +e_categories_dialog_init (ECategoriesDialog *dialog) +{ + GtkWidget *dialog_content; + GtkWidget *categories_editor; + + dialog->priv = E_CATEGORIES_DIALOG_GET_PRIVATE (dialog); + + categories_editor = e_categories_editor_new (); + dialog->priv->categories_editor = categories_editor; + + dialog_content = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_container_set_border_width (GTK_CONTAINER (dialog), 12); + gtk_box_pack_start ( + GTK_BOX (dialog_content), categories_editor, TRUE, TRUE, 0); + gtk_box_set_spacing (GTK_BOX (dialog_content), 12); + + g_signal_connect ( + categories_editor, "entry-changed", + G_CALLBACK (entry_changed_cb), dialog); + + gtk_dialog_add_buttons ( + GTK_DIALOG (dialog), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, NULL); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (dialog), GTK_RESPONSE_OK, FALSE); + gtk_window_set_title (GTK_WINDOW (dialog), _("Categories")); + + gtk_widget_show_all (categories_editor); +} + +/** + * e_categories_dialog_new: + * @categories: Comma-separated list of categories + * + * Creates a new #ECategoriesDialog widget and sets the initial selection + * to @categories. + * + * Returns: a new #ECategoriesDialog + **/ +GtkWidget * +e_categories_dialog_new (const gchar *categories) +{ + ECategoriesDialog *dialog; + + dialog = g_object_new (E_TYPE_CATEGORIES_DIALOG, NULL); + + if (categories) + e_categories_dialog_set_categories (dialog, categories); + + return GTK_WIDGET (dialog); +} + +/** + * e_categories_dialog_get_categories: + * @dialog: An #ECategoriesDialog + * + * Gets a comma-separated list of the categories currently selected + * in the dialog. + * + * Returns: a comma-separated list of categories. Free returned + * pointer with g_free(). + **/ +gchar * +e_categories_dialog_get_categories (ECategoriesDialog *dialog) +{ + gchar *categories; + + g_return_val_if_fail (E_IS_CATEGORIES_DIALOG (dialog), NULL); + + categories = e_categories_editor_get_categories ( + E_CATEGORIES_EDITOR (dialog->priv->categories_editor)); + + return categories; +} + +/** + * e_categories_dialog_set_categories: + * @dialog: An #ECategoriesDialog + * @categories: Comma-separated list of categories + * + * Sets the list of categories selected on the dialog. + **/ +void +e_categories_dialog_set_categories (ECategoriesDialog *dialog, + const gchar *categories) +{ + g_return_if_fail (E_IS_CATEGORIES_DIALOG (dialog)); + + e_categories_editor_set_categories ( + E_CATEGORIES_EDITOR (dialog->priv->categories_editor), + categories); +} diff --git a/e-util/e-categories-dialog.h b/e-util/e-categories-dialog.h new file mode 100644 index 0000000000..5ad35e098c --- /dev/null +++ b/e-util/e-categories-dialog.h @@ -0,0 +1,73 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CATEGORIES_DIALOG_H +#define E_CATEGORIES_DIALOG_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_CATEGORIES_DIALOG \ + (e_categories_dialog_get_type ()) +#define E_CATEGORIES_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CATEGORIES_DIALOG, ECategoriesDialog)) +#define E_CATEGORIES_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CATEGORIES_DIALOG, ECategoriesDialogClass)) +#define E_IS_CATEGORIES_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CATEGORIES_DIALOG)) +#define E_IS_CATEGORIES_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CATEGORIES_DIALOG)) +#define E_CATEGORIES_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CATEGORIES_DIALOG, ECategoriesDialogClass)) + +G_BEGIN_DECLS + +typedef struct _ECategoriesDialog ECategoriesDialog; +typedef struct _ECategoriesDialogClass ECategoriesDialogClass; +typedef struct _ECategoriesDialogPrivate ECategoriesDialogPrivate; + +struct _ECategoriesDialog { + GtkDialog parent; + ECategoriesDialogPrivate *priv; +}; + +struct _ECategoriesDialogClass { + GtkDialogClass parent_class; +}; + +GType e_categories_dialog_get_type (void); +GtkWidget * e_categories_dialog_new (const gchar *categories); +gchar * e_categories_dialog_get_categories + (ECategoriesDialog *dialog); +void e_categories_dialog_set_categories + (ECategoriesDialog *dialog, + const gchar *categories); + +G_END_DECLS + +#endif /* E_CATEGORIES_DIALOG_H */ diff --git a/e-util/e-categories-editor.c b/e-util/e-categories-editor.c new file mode 100644 index 0000000000..ecbebf6083 --- /dev/null +++ b/e-util/e-categories-editor.c @@ -0,0 +1,435 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include <config.h> +#include <string.h> +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n-lib.h> + +#include <libedataserver/libedataserver.h> + +#include "e-categories-editor.h" +#include "e-categories-selector.h" +#include "e-category-completion.h" +#include "e-category-editor.h" + +#define E_CATEGORIES_EDITOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CATEGORIES_EDITOR, ECategoriesEditorPrivate)) + +struct _ECategoriesEditorPrivate { + ECategoriesSelector *categories_list; + GtkWidget *categories_entry; + GtkWidget *categories_entry_label; + GtkWidget *new_button; + GtkWidget *edit_button; + GtkWidget *delete_button; + + guint ignore_category_changes : 1; +}; + +enum { + COLUMN_ACTIVE, + COLUMN_ICON, + COLUMN_CATEGORY, + N_COLUMNS +}; + +enum { + PROP_0, + PROP_ENTRY_VISIBLE +}; + +enum { + ENTRY_CHANGED, + LAST_SIGNAL +}; + +static gint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_TYPE (ECategoriesEditor, e_categories_editor, GTK_TYPE_GRID) + +static void +entry_changed_cb (GtkEntry *entry, + ECategoriesEditor *editor) +{ + g_signal_emit (editor, signals[ENTRY_CHANGED], 0); +} + +static void +categories_editor_selection_changed_cb (ECategoriesEditor *editor, + GtkTreeSelection *selection) +{ + GtkWidget *widget; + gint n_rows; + + n_rows = gtk_tree_selection_count_selected_rows (selection); + + widget = editor->priv->edit_button; + gtk_widget_set_sensitive (widget, n_rows == 1); + + widget = editor->priv->delete_button; + gtk_widget_set_sensitive (widget, n_rows >= 1); +} + +static void +category_checked_cb (ECategoriesSelector *selector, + const gchar *category, + const gboolean checked, + ECategoriesEditor *editor) +{ + GtkEntry *entry; + gchar *categories; + + entry = GTK_ENTRY (editor->priv->categories_entry); + categories = e_categories_selector_get_checked (selector); + + gtk_entry_set_text (entry, categories); + + g_free (categories); +} + +static void +new_button_clicked_cb (GtkButton *button, + ECategoriesEditor *editor) +{ + ECategoryEditor *cat_editor = e_category_editor_new (); + + e_category_editor_create_category (cat_editor); + + gtk_widget_destroy (GTK_WIDGET (cat_editor)); +} + +static void +edit_button_clicked_cb (GtkButton *button, + ECategoriesEditor *editor) +{ + ECategoryEditor *cat_editor = e_category_editor_new (); + gchar *category; + + category = e_categories_selector_get_selected ( + editor->priv->categories_list); + + e_category_editor_edit_category (cat_editor, category); + + gtk_widget_destroy (GTK_WIDGET (cat_editor)); + g_free (category); +} + +static void +categories_editor_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ENTRY_VISIBLE: + e_categories_editor_set_entry_visible ( + E_CATEGORIES_EDITOR (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +categories_editor_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ENTRY_VISIBLE: + g_value_set_boolean ( + value, e_categories_editor_get_entry_visible ( + E_CATEGORIES_EDITOR (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +e_categories_editor_class_init (ECategoriesEditorClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ECategoriesEditorPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = categories_editor_set_property; + object_class->get_property = categories_editor_get_property; + + g_object_class_install_property ( + object_class, + PROP_ENTRY_VISIBLE, + g_param_spec_boolean ( + "entry-visible", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + signals[ENTRY_CHANGED] = g_signal_new ( + "entry-changed", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (ECategoriesEditorClass, entry_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_categories_editor_init (ECategoriesEditor *editor) +{ + GtkEntryCompletion *completion; + GtkGrid *grid; + GtkWidget *entry_categories; + GtkWidget *label_header; + GtkWidget *label2; + GtkWidget *scrolledwindow1; + GtkWidget *categories_list; + GtkWidget *hbuttonbox1; + GtkWidget *button_new; + GtkWidget *button_edit; + GtkWidget *button_delete; + + gtk_widget_set_size_request (GTK_WIDGET (editor), -1, 400); + + grid = GTK_GRID (editor); + + gtk_grid_set_row_spacing (grid, 6); + gtk_grid_set_column_spacing (grid, 6); + + label_header = gtk_label_new_with_mnemonic ( + _("Currently _used categories:")); + gtk_widget_set_halign (label_header, GTK_ALIGN_FILL); + gtk_grid_attach (grid, label_header, 0, 0, 1, 1); + gtk_label_set_justify (GTK_LABEL (label_header), GTK_JUSTIFY_CENTER); + gtk_misc_set_alignment (GTK_MISC (label_header), 0, 0.5); + + entry_categories = gtk_entry_new (); + gtk_widget_set_hexpand (entry_categories, TRUE); + gtk_widget_set_halign (entry_categories, GTK_ALIGN_FILL); + gtk_grid_attach (grid, entry_categories, 0, 1, 1, 1); + + label2 = gtk_label_new_with_mnemonic (_("_Available Categories:")); + gtk_widget_set_halign (label2, GTK_ALIGN_FILL); + gtk_grid_attach (grid, label2, 0, 2, 1, 1); + gtk_label_set_justify (GTK_LABEL (label2), GTK_JUSTIFY_CENTER); + gtk_misc_set_alignment (GTK_MISC (label2), 0, 0.5); + + scrolledwindow1 = gtk_scrolled_window_new (NULL, NULL); + g_object_set (G_OBJECT (scrolledwindow1), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_grid_attach (grid, scrolledwindow1, 0, 3, 1, 1); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (scrolledwindow1), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (scrolledwindow1), GTK_SHADOW_IN); + + categories_list = GTK_WIDGET (e_categories_selector_new ()); + gtk_container_add (GTK_CONTAINER (scrolledwindow1), categories_list); + gtk_widget_set_size_request (categories_list, -1, 350); + gtk_tree_view_set_headers_visible ( + GTK_TREE_VIEW (categories_list), FALSE); + gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (categories_list), TRUE); + g_signal_connect ( + G_OBJECT (categories_list), "category-checked", + G_CALLBACK (category_checked_cb), editor); + + hbuttonbox1 = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); + g_object_set (G_OBJECT (hbuttonbox1), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + NULL); + gtk_grid_attach (grid, hbuttonbox1, 0, 4, 1, 1); + gtk_box_set_spacing (GTK_BOX (hbuttonbox1), 6); + + button_new = gtk_button_new_from_stock (GTK_STOCK_NEW); + gtk_container_add (GTK_CONTAINER (hbuttonbox1), button_new); + gtk_widget_set_can_default (button_new, TRUE); + + button_edit = gtk_button_new_from_stock (GTK_STOCK_EDIT); + gtk_container_add (GTK_CONTAINER (hbuttonbox1), button_edit); + gtk_widget_set_can_default (button_edit, TRUE); + + button_delete = gtk_button_new_from_stock (GTK_STOCK_DELETE); + gtk_container_add (GTK_CONTAINER (hbuttonbox1), button_delete); + gtk_widget_set_can_default (button_delete, TRUE); + + gtk_label_set_mnemonic_widget ( + GTK_LABEL (label_header), entry_categories); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (label2), categories_list); + + editor->priv = E_CATEGORIES_EDITOR_GET_PRIVATE (editor); + + editor->priv->categories_list = E_CATEGORIES_SELECTOR (categories_list); + editor->priv->categories_entry = entry_categories; + editor->priv->categories_entry_label = label_header; + + g_signal_connect_swapped ( + editor->priv->categories_list, "selection-changed", + G_CALLBACK (categories_editor_selection_changed_cb), editor); + + completion = e_category_completion_new (); + gtk_entry_set_completion ( + GTK_ENTRY (editor->priv->categories_entry), completion); + g_object_unref (completion); + + editor->priv->new_button = button_new; + g_signal_connect ( + editor->priv->new_button, "clicked", + G_CALLBACK (new_button_clicked_cb), editor); + + editor->priv->edit_button = button_edit; + g_signal_connect ( + editor->priv->edit_button, "clicked", + G_CALLBACK (edit_button_clicked_cb), editor); + + editor->priv->delete_button = button_delete; + g_signal_connect_swapped ( + editor->priv->delete_button, "clicked", + G_CALLBACK (e_categories_selector_delete_selection), + editor->priv->categories_list); + + g_signal_connect ( + editor->priv->categories_entry, "changed", + G_CALLBACK (entry_changed_cb), editor); + + gtk_widget_show_all (GTK_WIDGET (editor)); +} + +/** + * e_categories_editor_new: + * + * Creates a new #ECategoriesEditor widget. + * + * Returns: a new #ECategoriesEditor + * + * Since: 3.2 + **/ +GtkWidget * +e_categories_editor_new (void) +{ + return g_object_new (E_TYPE_CATEGORIES_EDITOR, NULL); +} + +/** + * e_categories_editor_get_categories: + * @editor: an #ECategoriesEditor + * + * Gets a comma-separated list of the categories currently selected + * in the editor. + * + * Returns: a comma-separated list of categories. Free returned + * pointer with g_free(). + * + * Since: 3.2 + **/ +gchar * +e_categories_editor_get_categories (ECategoriesEditor *editor) +{ + ECategoriesSelector *categories_list; + + g_return_val_if_fail (E_IS_CATEGORIES_EDITOR (editor), NULL); + + categories_list = editor->priv->categories_list; + + return e_categories_selector_get_checked (categories_list); +} + +/** + * e_categories_editor_set_categories: + * @editor: an #ECategoriesEditor + * @categories: comma-separated list of categories + * + * Sets the list of categories selected on the editor. + * + * Since: 3.2 + **/ +void +e_categories_editor_set_categories (ECategoriesEditor *editor, + const gchar *categories) +{ + ECategoriesSelector *categories_list; + + g_return_if_fail (E_IS_CATEGORIES_EDITOR (editor)); + + categories_list = editor->priv->categories_list; + + e_categories_selector_set_checked (categories_list, categories); + category_checked_cb (categories_list, NULL, FALSE, editor); +} + +/** + * e_categories_editor_get_entry_visible: + * @editor: an #ECategoriesEditor + * + * Return the visibility of the category input entry. + * + * Returns: whether the entry is visible + * + * Since: 3.2 + **/ +gboolean +e_categories_editor_get_entry_visible (ECategoriesEditor *editor) +{ + g_return_val_if_fail (E_IS_CATEGORIES_EDITOR (editor), TRUE); + + return gtk_widget_get_visible (editor->priv->categories_entry); +} + +/** + * e_categories_editor_set_entry_visible: + * @editor: an #ECategoriesEditor + * @entry_visible: whether to make the entry visible + * + * Sets the visibility of the category input entry. + * + * Since: 3.2 + **/ +void +e_categories_editor_set_entry_visible (ECategoriesEditor *editor, + gboolean entry_visible) +{ + g_return_if_fail (E_IS_CATEGORIES_EDITOR (editor)); + + if ((gtk_widget_get_visible (editor->priv->categories_entry) ? 1 : 0) == + (entry_visible ? 1 : 0)) + return; + + gtk_widget_set_visible ( + editor->priv->categories_entry, entry_visible); + gtk_widget_set_visible ( + editor->priv->categories_entry_label, entry_visible); + e_categories_selector_set_items_checkable ( + editor->priv->categories_list, entry_visible); + + g_object_notify (G_OBJECT (editor), "entry-visible"); +} diff --git a/e-util/e-categories-editor.h b/e-util/e-categories-editor.h new file mode 100644 index 0000000000..07c9dc6987 --- /dev/null +++ b/e-util/e-categories-editor.h @@ -0,0 +1,88 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CATEGORIES_EDITOR_H +#define E_CATEGORIES_EDITOR_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_CATEGORIES_EDITOR \ + (e_categories_editor_get_type ()) +#define E_CATEGORIES_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CATEGORIES_EDITOR, ECategoriesEditor)) +#define E_CATEGORIES_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CATEGORIES_EDITOR, ECategoriesEditorClass)) +#define E_IS_CATEGORIES_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CATEGORIES_EDITOR)) +#define E_IS_CATEGORIES_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CATEGORIES_EDITOR)) +#define E_CATEGORIES_EDITOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CATEGORIES_EDITOR, ECategoriesEditorClass)) + +G_BEGIN_DECLS + +typedef struct _ECategoriesEditor ECategoriesEditor; +typedef struct _ECategoriesEditorClass ECategoriesEditorClass; +typedef struct _ECategoriesEditorPrivate ECategoriesEditorPrivate; + +/** + * ECategoriesEditor: + * + * Contains only private data that should be read and manipulated using the + * functions below. + * + * Since: 3.2 + **/ +struct _ECategoriesEditor { + GtkGrid parent; + ECategoriesEditorPrivate *priv; +}; + +struct _ECategoriesEditorClass { + GtkGridClass parent_class; + + void (*entry_changed) (GtkEntry *entry); +}; + +GType e_categories_editor_get_type (void); +GtkWidget * e_categories_editor_new (void); +gchar * e_categories_editor_get_categories + (ECategoriesEditor *editor); +void e_categories_editor_set_categories + (ECategoriesEditor *editor, + const gchar *categories); +gboolean e_categories_editor_get_entry_visible + (ECategoriesEditor *editor); +void e_categories_editor_set_entry_visible + (ECategoriesEditor *editor, + gboolean entry_visible); + +G_END_DECLS + +#endif /* E_CATEGORIES_EDITOR_H */ diff --git a/e-util/e-categories-selector.c b/e-util/e-categories-selector.c new file mode 100644 index 0000000000..5a05238626 --- /dev/null +++ b/e-util/e-categories-selector.c @@ -0,0 +1,587 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include <libedataserver/libedataserver.h> + +#include "e-categories-selector.h" + +#define E_CATEGORIES_SELECTOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CATEGORIES_SELECTOR, ECategoriesSelectorPrivate)) + +struct _ECategoriesSelectorPrivate { + gboolean checkable; + GHashTable *selected_categories; + + gboolean ignore_category_changes; +}; + +enum { + PROP_0, + PROP_ITEMS_CHECKABLE +}; + +enum { + CATEGORY_CHECKED, + SELECTION_CHANGED, + LAST_SIGNAL +}; + +enum { + COLUMN_ACTIVE, + COLUMN_ICON, + COLUMN_CATEGORY, + N_COLUMNS +}; + +static gint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_TYPE ( + ECategoriesSelector, + e_categories_selector, + GTK_TYPE_TREE_VIEW) + +static void +categories_selector_build_model (ECategoriesSelector *selector) +{ + GtkListStore *store; + GList *list, *iter; + + store = gtk_list_store_new ( + N_COLUMNS, G_TYPE_BOOLEAN, GDK_TYPE_PIXBUF, G_TYPE_STRING); + + gtk_tree_sortable_set_sort_column_id ( + GTK_TREE_SORTABLE (store), + COLUMN_CATEGORY, GTK_SORT_ASCENDING); + + list = e_categories_get_list (); + for (iter = list; iter != NULL; iter = iter->next) { + const gchar *category_name = iter->data; + const gchar *filename; + GdkPixbuf *pixbuf = NULL; + GtkTreeIter iter; + gboolean active; + + /* Only add user-visible categories. */ + if (!e_categories_is_searchable (category_name)) + continue; + + active = (g_hash_table_lookup ( + selector->priv->selected_categories, + category_name) != NULL); + + filename = e_categories_get_icon_file_for (category_name); + if (filename != NULL) + pixbuf = gdk_pixbuf_new_from_file (filename, NULL); + + gtk_list_store_append (store, &iter); + + gtk_list_store_set ( + store, &iter, + COLUMN_ACTIVE, active, + COLUMN_ICON, pixbuf, + COLUMN_CATEGORY, category_name, + -1); + + if (pixbuf != NULL) + g_object_unref (pixbuf); + } + + gtk_tree_view_set_model ( + GTK_TREE_VIEW (selector), GTK_TREE_MODEL (store)); + + /* This has to be reset everytime we install a new model */ + gtk_tree_view_set_search_column ( + GTK_TREE_VIEW (selector), COLUMN_CATEGORY); + + g_list_free (list); + g_object_unref (store); +} + +static void +category_toggled_cb (GtkCellRenderer *renderer, + const gchar *path, + ECategoriesSelector *selector) +{ + GtkTreeModel *model; + GtkTreePath *tree_path; + GtkTreeIter iter; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (selector)); + g_return_if_fail (model); + + tree_path = gtk_tree_path_new_from_string (path); + g_return_if_fail (tree_path); + + if (gtk_tree_model_get_iter (model, &iter, tree_path)) { + gchar *category; + gboolean active; + + gtk_tree_model_get ( + model, &iter, + COLUMN_ACTIVE, &active, + COLUMN_CATEGORY, &category, -1); + + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + COLUMN_ACTIVE, !active, -1); + + if (active) + g_hash_table_remove ( + selector->priv->selected_categories, category); + else + g_hash_table_insert ( + selector->priv->selected_categories, + g_strdup (category), g_strdup (category)); + + g_signal_emit ( + selector, signals[CATEGORY_CHECKED], 0, + category, !active); + + g_free (category); + } + + gtk_tree_path_free (tree_path); +} + +static void +categories_selector_listener_cb (gpointer useless_pointer, + ECategoriesSelector *selector) +{ + if (!selector->priv->ignore_category_changes) + categories_selector_build_model (selector); +} + +static gboolean +categories_selector_key_press_event (ECategoriesSelector *selector, + GdkEventKey *event) +{ + if (event->keyval == GDK_KEY_Delete) { + e_categories_selector_delete_selection (selector); + return TRUE; + } + + return FALSE; +} + +static void +categories_selector_selection_changed (GtkTreeSelection *selection, + ECategoriesSelector *selector) +{ + g_signal_emit (selector, signals[SELECTION_CHANGED], 0, selection); +} + +static void +categories_selector_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ITEMS_CHECKABLE: + g_value_set_boolean ( + value, + e_categories_selector_get_items_checkable ( + E_CATEGORIES_SELECTOR (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +categories_selector_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ITEMS_CHECKABLE: + e_categories_selector_set_items_checkable ( + E_CATEGORIES_SELECTOR (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +categories_selector_dispose (GObject *object) +{ + ECategoriesSelectorPrivate *priv; + + priv = E_CATEGORIES_SELECTOR_GET_PRIVATE (object); + + if (priv->selected_categories != NULL) { + g_hash_table_destroy (priv->selected_categories); + priv->selected_categories = NULL; + } + + /* Chain up to parent's dispose() method.*/ + G_OBJECT_CLASS (e_categories_selector_parent_class)->dispose (object); +} + +static void +categories_selector_finalize (GObject *object) +{ + e_categories_unregister_change_listener ( + G_CALLBACK (categories_selector_listener_cb), object); +} + +static void +e_categories_selector_class_init (ECategoriesSelectorClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ECategoriesSelectorPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = categories_selector_set_property; + object_class->get_property = categories_selector_get_property; + object_class->dispose = categories_selector_dispose; + object_class->finalize = categories_selector_finalize; + + g_object_class_install_property ( + object_class, + PROP_ITEMS_CHECKABLE, + g_param_spec_boolean ( + "items-checkable", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + signals[CATEGORY_CHECKED] = g_signal_new ( + "category-checked", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (ECategoriesSelectorClass, category_checked), + NULL, NULL, NULL, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_BOOLEAN); + + signals[SELECTION_CHANGED] = g_signal_new ( + "selection-changed", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (ECategoriesSelectorClass, selection_changed), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + GTK_TYPE_TREE_SELECTION); +} + +static void +e_categories_selector_init (ECategoriesSelector *selector) +{ + GtkCellRenderer *renderer; + GtkTreeViewColumn *column; + GtkTreeSelection *selection; + + selector->priv = E_CATEGORIES_SELECTOR_GET_PRIVATE (selector); + + selector->priv->checkable = TRUE; + selector->priv->selected_categories = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + selector->priv->ignore_category_changes = FALSE; + + renderer = gtk_cell_renderer_toggle_new (); + column = gtk_tree_view_column_new_with_attributes ( + "?", renderer, "active", COLUMN_ACTIVE, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (selector), column); + + g_signal_connect ( + renderer, "toggled", + G_CALLBACK (category_toggled_cb), selector); + + renderer = gtk_cell_renderer_pixbuf_new (); + column = gtk_tree_view_column_new_with_attributes ( + _("Icon"), renderer, "pixbuf", COLUMN_ICON, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (selector), column); + + renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes ( + _("Category"), renderer, "text", COLUMN_CATEGORY, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (selector), column); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (selector)); + g_signal_connect ( + selection, "changed", + G_CALLBACK (categories_selector_selection_changed), selector); + + g_signal_connect ( + selector, "key-press-event", + G_CALLBACK (categories_selector_key_press_event), NULL); + + e_categories_register_change_listener ( + G_CALLBACK (categories_selector_listener_cb), selector); + + categories_selector_build_model (selector); +} + +/** + * e_categories_selector_new: + * + * Since: 3.2 + **/ +GtkWidget * +e_categories_selector_new (void) +{ + return g_object_new ( + E_TYPE_CATEGORIES_SELECTOR, + "items-checkable", TRUE, NULL); +} + +/** + * e_categories_selector_get_items_checkable: + * + * Since: 3.2 + **/ +gboolean +e_categories_selector_get_items_checkable (ECategoriesSelector *selector) +{ + g_return_val_if_fail (E_IS_CATEGORIES_SELECTOR (selector), TRUE); + + return selector->priv->checkable; +} + +/** + * e_categories_selector_set_items_checkable: + * + * Since: 3.2 + **/ +void +e_categories_selector_set_items_checkable (ECategoriesSelector *selector, + gboolean checkable) +{ + GtkTreeViewColumn *column; + + g_return_if_fail (E_IS_CATEGORIES_SELECTOR (selector)); + + if ((selector->priv->checkable ? 1 : 0) == (checkable ? 1 : 0)) + return; + + selector->priv->checkable = checkable; + + column = gtk_tree_view_get_column ( + GTK_TREE_VIEW (selector), COLUMN_ACTIVE); + gtk_tree_view_column_set_visible (column, checkable); + + g_object_notify (G_OBJECT (selector), "items-checkable"); +} + +/** + * e_categories_selector_get_checked: + * + * Free returned pointer with g_free(). + * + * Since: 3.2 + **/ +gchar * +e_categories_selector_get_checked (ECategoriesSelector *selector) +{ + GString *str; + GList *list, *category; + + g_return_val_if_fail (E_IS_CATEGORIES_SELECTOR (selector), NULL); + + str = g_string_new (""); + list = g_hash_table_get_values (selector->priv->selected_categories); + + /* to get them always in the same order */ + list = g_list_sort (list, (GCompareFunc) g_utf8_collate); + + for (category = list; category != NULL; category = category->next) { + if (str->len > 0) + g_string_append_printf ( + str, ",%s", (gchar *) category->data); + else + g_string_append (str, (gchar *) category->data); + } + + g_list_free (list); + + return g_string_free (str, FALSE); +} + +/** + * e_categories_selector_set_checked: + * + * Since: 3.2 + **/ +void +e_categories_selector_set_checked (ECategoriesSelector *selector, + const gchar *categories) +{ + GtkTreeModel *model; + GtkTreeIter iter; + gchar **arr; + gint i; + + g_return_if_fail (E_IS_CATEGORIES_SELECTOR (selector)); + + /* Clean up table of selected categories. */ + g_hash_table_remove_all (selector->priv->selected_categories); + + arr = g_strsplit (categories, ",", 0); + if (arr) { + for (i = 0; arr[i] != NULL; i++) { + g_strstrip (arr[i]); + g_hash_table_insert ( + selector->priv->selected_categories, + g_strdup (arr[i]), g_strdup (arr[i])); + } + g_strfreev (arr); + } + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (selector)); + if (gtk_tree_model_get_iter_first (model, &iter)) { + do { + gchar *category_name; + gboolean found; + + gtk_tree_model_get ( + model, &iter, + COLUMN_CATEGORY, &category_name, + -1); + found = (g_hash_table_lookup ( + selector->priv->selected_categories, + category_name) != NULL); + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + COLUMN_ACTIVE, found, -1); + + g_free (category_name); + } while (gtk_tree_model_iter_next (model, &iter)); + } +} + +/** + * e_categories_selector_delete_selection: + * + * Since: 3.2 + **/ +void +e_categories_selector_delete_selection (ECategoriesSelector *selector) +{ + GtkTreeModel *model; + GtkTreeSelection *selection; + GList *selected, *item; + + g_return_if_fail (E_IS_CATEGORIES_SELECTOR (selector)); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (selector)); + g_return_if_fail (model != NULL); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (selector)); + selected = gtk_tree_selection_get_selected_rows (selection, &model); + + /* Remove categories in reverse order to avoid invalidating + * tree paths as we iterate over the list. Note, the list is + * probably already sorted but we sort again just to be safe. */ + selected = g_list_reverse (g_list_sort ( + selected, (GCompareFunc) gtk_tree_path_compare)); + + /* Prevent the model from being rebuilt every time we + * remove a category, since we're already modifying it. */ + selector->priv->ignore_category_changes = TRUE; + + for (item = selected; item != NULL; item = item->next) { + GtkTreePath *path = item->data; + GtkTreeIter iter; + gchar *category; + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get ( + model, &iter, + COLUMN_CATEGORY, &category, -1); + gtk_list_store_remove (GTK_LIST_STORE (model), &iter); + e_categories_remove (category); + g_free (category); + } + + selector->priv->ignore_category_changes = FALSE; + + /* If we only remove one category, try to select another */ + if (g_list_length (selected) == 1) { + GtkTreePath *path = selected->data; + + gtk_tree_selection_select_path (selection, path); + if (!gtk_tree_selection_path_is_selected (selection, path)) + if (gtk_tree_path_prev (path)) + gtk_tree_selection_select_path (selection, path); + } + + g_list_foreach (selected, (GFunc) gtk_tree_path_free, NULL); + g_list_free (selected); +} + +/** + * e_categories_selector_get_selected: + * + * Free returned pointer with g_free(). + * + * Since: 3.2 + **/ +gchar * +e_categories_selector_get_selected (ECategoriesSelector *selector) +{ + GtkTreeModel *model; + GtkTreeSelection *selection; + GList *selected, *item; + GString *str = g_string_new (""); + + g_return_val_if_fail (E_IS_CATEGORIES_SELECTOR (selector), NULL); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (selector)); + g_return_val_if_fail (model != NULL, NULL); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (selector)); + selected = gtk_tree_selection_get_selected_rows (selection, &model); + + for (item = selected; item != NULL; item = item->next) { + GtkTreePath *path = item->data; + GtkTreeIter iter; + gchar *category; + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get ( + model, &iter, + COLUMN_CATEGORY, &category, -1); + if (str->len == 0) + g_string_assign (str, category); + else + g_string_append_printf (str, ",%s", category); + + g_free (category); + } + + g_list_foreach (selected, (GFunc) gtk_tree_path_free, NULL); + g_list_free (selected); + + return g_string_free (str, FALSE); +} diff --git a/e-util/e-categories-selector.h b/e-util/e-categories-selector.h new file mode 100644 index 0000000000..6ffc9f82ef --- /dev/null +++ b/e-util/e-categories-selector.h @@ -0,0 +1,97 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CATEGORIES_SELECTOR_H +#define E_CATEGORIES_SELECTOR_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_CATEGORIES_SELECTOR \ + (e_categories_selector_get_type ()) +#define E_CATEGORIES_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CATEGORIES_SELECTOR, ECategoriesSelector)) +#define E_CATEGORIES_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CATEGORIES_SELECTOR, ECategoriesSelectorClass)) +#define E_IS_CATEGORIES_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CATEGORIES_SELECTOR)) +#define E_IS_CATEGORIES_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CATEGORIES_SELECTOR)) +#define E_CATEGORIES_SELECTOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CATEGORIES_SELECTOR, ECategoriesSelectorClass)) + +G_BEGIN_DECLS + +typedef struct _ECategoriesSelector ECategoriesSelector; +typedef struct _ECategoriesSelectorClass ECategoriesSelectorClass; +typedef struct _ECategoriesSelectorPrivate ECategoriesSelectorPrivate; + +/** + * ECategoriesSelector: + * + * Contains only private data that should be read and manipulated using the + * functions below. + * + * Since: 3.2 + **/ +struct _ECategoriesSelector { + GtkTreeView parent; + ECategoriesSelectorPrivate *priv; +}; + +struct _ECategoriesSelectorClass { + GtkTreeViewClass parent_class; + + void (*category_checked) (ECategoriesSelector *selector, + const gchar *category, + gboolean checked); + + void (*selection_changed) (ECategoriesSelector *selector, + GtkTreeSelection *selection); +}; + +GType e_categories_selector_get_type (void); +GtkWidget * e_categories_selector_new (void); +gchar * e_categories_selector_get_checked + (ECategoriesSelector *selector); +void e_categories_selector_set_checked + (ECategoriesSelector *selector, + const gchar *categories); +gboolean e_categories_selector_get_items_checkable + (ECategoriesSelector *selector); +void e_categories_selector_set_items_checkable + (ECategoriesSelector *selectr, + gboolean checkable); +void e_categories_selector_delete_selection + (ECategoriesSelector *selector); +gchar * e_categories_selector_get_selected + (ECategoriesSelector *selector); + +G_END_DECLS + +#endif /* E_CATEGORIES_SELECTOR_H */ diff --git a/e-util/e-category-completion.c b/e-util/e-category-completion.c new file mode 100644 index 0000000000..095df50b45 --- /dev/null +++ b/e-util/e-category-completion.c @@ -0,0 +1,505 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +#include "e-category-completion.h" + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <glib/gi18n-lib.h> + +#include <libedataserver/libedataserver.h> + +#define E_CATEGORY_COMPLETION_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CATEGORY_COMPLETION, ECategoryCompletionPrivate)) + +struct _ECategoryCompletionPrivate { + GtkWidget *last_known_entry; + gchar *create; + gchar *prefix; +}; + +enum { + COLUMN_PIXBUF, + COLUMN_CATEGORY, + COLUMN_NORMALIZED, + NUM_COLUMNS +}; + +G_DEFINE_TYPE ( + ECategoryCompletion, + e_category_completion, + GTK_TYPE_ENTRY_COMPLETION) + +/* Forward Declarations */ + +static void +category_completion_track_entry (GtkEntryCompletion *completion); + +static void +category_completion_build_model (GtkEntryCompletion *completion) +{ + GtkListStore *store; + GList *list; + + store = gtk_list_store_new ( + NUM_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING); + + list = e_categories_get_list (); + while (list != NULL) { + const gchar *category = list->data; + const gchar *filename; + gchar *normalized; + gchar *casefolded; + GdkPixbuf *pixbuf = NULL; + GtkTreeIter iter; + + /* Only add user-visible categories. */ + if (!e_categories_is_searchable (category)) { + list = g_list_delete_link (list, list); + continue; + } + + filename = e_categories_get_icon_file_for (category); + if (filename != NULL && *filename != '\0') + pixbuf = gdk_pixbuf_new_from_file (filename, NULL); + + normalized = g_utf8_normalize ( + category, -1, G_NORMALIZE_DEFAULT); + casefolded = g_utf8_casefold (normalized, -1); + + gtk_list_store_append (store, &iter); + + gtk_list_store_set ( + store, &iter, COLUMN_PIXBUF, pixbuf, + COLUMN_CATEGORY, category, COLUMN_NORMALIZED, + casefolded, -1); + + g_free (normalized); + g_free (casefolded); + + if (pixbuf != NULL) + g_object_unref (pixbuf); + + list = g_list_delete_link (list, list); + } + + gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store)); +} + +static void +category_completion_categories_changed_cb (GObject *some_private_object, + GtkEntryCompletion *completion) +{ + category_completion_build_model (completion); +} + +static void +category_completion_complete (GtkEntryCompletion *completion, + const gchar *category) +{ + GtkEditable *editable; + GtkWidget *entry; + const gchar *text; + const gchar *cp; + gint start_pos; + gint end_pos; + glong offset; + + entry = gtk_entry_completion_get_entry (completion); + + editable = GTK_EDITABLE (entry); + text = gtk_entry_get_text (GTK_ENTRY (entry)); + + /* Get the cursor position as a character offset. */ + offset = gtk_editable_get_position (editable); + + /* Find the rightmost comma before the cursor. */ + cp = g_utf8_offset_to_pointer (text, offset); + cp = g_utf8_strrchr (text, (gssize) (cp - text), ','); + + /* Calculate the selection start position as a character offset. */ + if (cp == NULL) + offset = 0; + else { + cp = g_utf8_next_char (cp); + if (g_unichar_isspace (g_utf8_get_char (cp))) + cp = g_utf8_next_char (cp); + offset = g_utf8_pointer_to_offset (text, cp); + } + start_pos = (gint) offset; + + /* Find the leftmost comma after the cursor. */ + cp = g_utf8_offset_to_pointer (text, offset); + cp = g_utf8_strchr (cp, -1, ','); + + /* Calculate the selection end position as a character offset. */ + if (cp == NULL) + offset = -1; + else { + cp = g_utf8_next_char (cp); + if (g_unichar_isspace (g_utf8_get_char (cp))) + cp = g_utf8_next_char (cp); + offset = g_utf8_pointer_to_offset (text, cp); + } + end_pos = (gint) offset; + + /* Complete the partially typed category. */ + gtk_editable_delete_text (editable, start_pos, end_pos); + gtk_editable_insert_text (editable, category, -1, &start_pos); + gtk_editable_insert_text (editable, ",", 1, &start_pos); + gtk_editable_set_position (editable, start_pos); +} + +static gboolean +category_completion_is_match (GtkEntryCompletion *completion, + const gchar *key, + GtkTreeIter *iter) +{ + ECategoryCompletionPrivate *priv; + GtkTreeModel *model; + GtkWidget *entry; + GValue value = { 0, }; + gboolean match; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion); + entry = gtk_entry_completion_get_entry (completion); + model = gtk_entry_completion_get_model (completion); + + /* XXX This would be easier if GtkEntryCompletion had an 'entry' + * property that we could listen to for notifications. */ + if (entry != priv->last_known_entry) + category_completion_track_entry (completion); + + if (priv->prefix == NULL) + return FALSE; + + gtk_tree_model_get_value (model, iter, COLUMN_NORMALIZED, &value); + match = g_str_has_prefix (g_value_get_string (&value), priv->prefix); + g_value_unset (&value); + + return match; +} + +static void +category_completion_update_prefix (GtkEntryCompletion *completion) +{ + ECategoryCompletionPrivate *priv; + GtkEditable *editable; + GtkTreeModel *model; + GtkWidget *entry; + GtkTreeIter iter; + const gchar *text; + const gchar *start; + const gchar *end; + const gchar *cp; + gboolean valid; + gchar *input; + glong offset; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion); + entry = gtk_entry_completion_get_entry (completion); + model = gtk_entry_completion_get_model (completion); + + /* XXX This would be easier if GtkEntryCompletion had an 'entry' + * property that we could listen to for notifications. */ + if (entry != priv->last_known_entry) { + category_completion_track_entry (completion); + return; + } + + editable = GTK_EDITABLE (entry); + text = gtk_entry_get_text (GTK_ENTRY (entry)); + + /* Get the cursor position as a character offset. */ + offset = gtk_editable_get_position (editable); + + /* Find the rightmost comma before the cursor. */ + cp = g_utf8_offset_to_pointer (text, offset); + cp = g_utf8_strrchr (text, (gsize) (cp - text), ','); + + /* Mark the start of the prefix. */ + if (cp == NULL) + start = text; + else { + cp = g_utf8_next_char (cp); + if (g_unichar_isspace (g_utf8_get_char (cp))) + cp = g_utf8_next_char (cp); + start = cp; + } + + /* Find the leftmost comma after the cursor. */ + cp = g_utf8_offset_to_pointer (text, offset); + cp = g_utf8_strchr (cp, -1, ','); + + /* Mark the end of the prefix. */ + if (cp == NULL) + end = text + strlen (text); + else + end = cp; + + if (priv->create != NULL) + gtk_entry_completion_delete_action (completion, 0); + + g_free (priv->create); + priv->create = NULL; + + g_free (priv->prefix); + priv->prefix = NULL; + + if (start == end) + return; + + input = g_strstrip (g_strndup (start, end - start)); + priv->create = input; + + input = g_utf8_normalize (input, -1, G_NORMALIZE_DEFAULT); + priv->prefix = g_utf8_casefold (input, -1); + g_free (input); + + if (*priv->create == '\0') { + g_free (priv->create); + priv->create = NULL; + return; + } + + valid = gtk_tree_model_get_iter_first (model, &iter); + while (valid) { + GValue value = { 0, }; + + gtk_tree_model_get_value ( + model, &iter, COLUMN_NORMALIZED, &value); + if (strcmp (g_value_get_string (&value), priv->prefix) == 0) { + g_value_unset (&value); + g_free (priv->create); + priv->create = NULL; + return; + } + g_value_unset (&value); + + valid = gtk_tree_model_iter_next (model, &iter); + } + + input = g_strdup_printf (_("Create category \"%s\""), priv->create); + gtk_entry_completion_insert_action_text (completion, 0, input); + g_free (input); +} + +static gboolean +category_completion_sanitize_suffix (GtkEntry *entry, + GdkEventFocus *event, + GtkEntryCompletion *completion) +{ + const gchar *text; + + g_return_val_if_fail (entry != NULL, FALSE); + g_return_val_if_fail (completion != NULL, FALSE); + + text = gtk_entry_get_text (entry); + if (text) { + gint len = strlen (text), old_len = len; + + while (len > 0 && (text[len -1] == ' ' || text[len - 1] == ',')) + len--; + + if (old_len != len) { + gchar *tmp = g_strndup (text, len); + + gtk_entry_set_text (entry, tmp); + + g_free (tmp); + } + } + + return FALSE; +} + +static void +category_completion_track_entry (GtkEntryCompletion *completion) +{ + ECategoryCompletionPrivate *priv; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion); + + if (priv->last_known_entry != NULL) { + g_signal_handlers_disconnect_matched ( + priv->last_known_entry, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, completion); + g_object_unref (priv->last_known_entry); + } + + g_free (priv->prefix); + priv->prefix = NULL; + + priv->last_known_entry = gtk_entry_completion_get_entry (completion); + if (priv->last_known_entry == NULL) + return; + + g_object_ref (priv->last_known_entry); + + g_signal_connect_swapped ( + priv->last_known_entry, "notify::cursor-position", + G_CALLBACK (category_completion_update_prefix), completion); + + g_signal_connect_swapped ( + priv->last_known_entry, "notify::text", + G_CALLBACK (category_completion_update_prefix), completion); + + g_signal_connect ( + priv->last_known_entry, "focus-out-event", + G_CALLBACK (category_completion_sanitize_suffix), completion); + + category_completion_update_prefix (completion); +} + +static void +category_completion_constructed (GObject *object) +{ + GtkCellRenderer *renderer; + GtkEntryCompletion *completion; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_category_completion_parent_class)->constructed (object); + + completion = GTK_ENTRY_COMPLETION (object); + + gtk_entry_completion_set_match_func ( + completion, (GtkEntryCompletionMatchFunc) + category_completion_is_match, NULL, NULL); + + gtk_entry_completion_set_text_column (completion, COLUMN_CATEGORY); + + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (completion), renderer, FALSE); + gtk_cell_layout_add_attribute ( + GTK_CELL_LAYOUT (completion), + renderer, "pixbuf", COLUMN_PIXBUF); + gtk_cell_layout_reorder ( + GTK_CELL_LAYOUT (completion), renderer, 0); + + e_categories_register_change_listener ( + G_CALLBACK (category_completion_categories_changed_cb), + completion); + + category_completion_build_model (completion); +} + +static void +category_completion_dispose (GObject *object) +{ + ECategoryCompletionPrivate *priv; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object); + + if (priv->last_known_entry != NULL) { + g_signal_handlers_disconnect_matched ( + priv->last_known_entry, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->last_known_entry); + priv->last_known_entry = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_category_completion_parent_class)->dispose (object); +} + +static void +category_completion_finalize (GObject *object) +{ + ECategoryCompletionPrivate *priv; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object); + + g_free (priv->create); + g_free (priv->prefix); + + e_categories_unregister_change_listener ( + G_CALLBACK (category_completion_categories_changed_cb), + object); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_category_completion_parent_class)->finalize (object); +} + +static gboolean +category_completion_match_selected (GtkEntryCompletion *completion, + GtkTreeModel *model, + GtkTreeIter *iter) +{ + GValue value = { 0, }; + + gtk_tree_model_get_value (model, iter, COLUMN_CATEGORY, &value); + category_completion_complete (completion, g_value_get_string (&value)); + g_value_unset (&value); + + return TRUE; +} + +static void +category_completion_action_activated (GtkEntryCompletion *completion, + gint index) +{ + ECategoryCompletionPrivate *priv; + gchar *category; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion); + + category = g_strdup (priv->create); + e_categories_add (category, NULL, NULL, TRUE); + category_completion_complete (completion, category); + g_free (category); +} + +static void +e_category_completion_class_init (ECategoryCompletionClass *class) +{ + GObjectClass *object_class; + GtkEntryCompletionClass *entry_completion_class; + + g_type_class_add_private (class, sizeof (ECategoryCompletionPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->constructed = category_completion_constructed; + object_class->dispose = category_completion_dispose; + object_class->finalize = category_completion_finalize; + + entry_completion_class = GTK_ENTRY_COMPLETION_CLASS (class); + entry_completion_class->match_selected = category_completion_match_selected; + entry_completion_class->action_activated = category_completion_action_activated; +} + +static void +e_category_completion_init (ECategoryCompletion *category_completion) +{ + category_completion->priv = + E_CATEGORY_COMPLETION_GET_PRIVATE (category_completion); +} + +/** + * e_category_completion_new: + * + * Since: 2.26 + **/ +GtkEntryCompletion * +e_category_completion_new (void) +{ + return g_object_new (E_TYPE_CATEGORY_COMPLETION, NULL); +} diff --git a/e-util/e-category-completion.h b/e-util/e-category-completion.h new file mode 100644 index 0000000000..477a036cd3 --- /dev/null +++ b/e-util/e-category-completion.h @@ -0,0 +1,72 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CATEGORY_COMPLETION_H +#define E_CATEGORY_COMPLETION_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_CATEGORY_COMPLETION \ + (e_category_completion_get_type ()) +#define E_CATEGORY_COMPLETION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CATEGORY_COMPLETION, ECategoryCompletion)) +#define E_CATEGORY_COMPLETION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CATEGORY_COMPLETION, ECategoryCompletionClass)) +#define E_IS_CATEGORY_COMPLETION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CATEGORY_COMPLETION)) +#define E_IS_CATEGORY_COMPLETION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CATEGORY_COMPLETION)) +#define E_CATEGORY_COMPLETION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CATEGORY_COMPLETION, ECategoryCompletionClass)) + +G_BEGIN_DECLS + +typedef struct _ECategoryCompletion ECategoryCompletion; +typedef struct _ECategoryCompletionClass ECategoryCompletionClass; +typedef struct _ECategoryCompletionPrivate ECategoryCompletionPrivate; + +/** + * ECategoryCompletion: + * + * Since: 2.26 + **/ +struct _ECategoryCompletion { + GtkEntryCompletion parent; + ECategoryCompletionPrivate *priv; +}; + +struct _ECategoryCompletionClass { + GtkEntryCompletionClass parent_class; +}; + +GType e_category_completion_get_type (void); +GtkEntryCompletion * + e_category_completion_new (void); + +G_END_DECLS + +#endif /* E_CATEGORY_COMPLETION_H */ diff --git a/e-util/e-category-editor.c b/e-util/e-category-editor.c new file mode 100644 index 0000000000..33ad6dde6c --- /dev/null +++ b/e-util/e-category-editor.c @@ -0,0 +1,343 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <gtk/gtk.h> +#include <glib/gi18n-lib.h> + +#include <libedataserver/libedataserver.h> + +#include "e-category-editor.h" + +#define E_CATEGORY_EDITOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CATEGORY_EDITOR, ECategoryEditorPrivate)) + +struct _ECategoryEditorPrivate { + GtkWidget *category_name; + GtkWidget *category_icon; +}; + +G_DEFINE_TYPE (ECategoryEditor, e_category_editor, GTK_TYPE_DIALOG) + +static void +update_preview (GtkFileChooser *chooser, + gpointer user_data) +{ + GtkImage *image; + gchar *filename; + + g_return_if_fail (chooser != NULL); + + image = GTK_IMAGE (gtk_file_chooser_get_preview_widget (chooser)); + g_return_if_fail (image != NULL); + + filename = gtk_file_chooser_get_preview_filename (chooser); + + gtk_image_set_from_file (image, filename); + gtk_file_chooser_set_preview_widget_active (chooser, filename != NULL); + + g_free (filename); +} + +static void +file_chooser_response (GtkDialog *dialog, + gint response_id, + GtkFileChooser *button) +{ + g_return_if_fail (button != NULL); + + if (response_id == GTK_RESPONSE_NO) + gtk_file_chooser_unselect_all (button); +} + +static void +category_editor_category_name_changed (GtkEntry *category_name_entry, + ECategoryEditor *editor) +{ + gchar *name; + + g_return_if_fail (editor != NULL); + g_return_if_fail (category_name_entry != NULL); + + name = g_strdup (gtk_entry_get_text (category_name_entry)); + if (name != NULL) + name = g_strstrip (name); + + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (editor), GTK_RESPONSE_OK, name && *name); + + g_free (name); +} + +static gchar * +check_category_name (const gchar *name) +{ + GString *str = NULL; + gchar *p = (gchar *) name; + + str = g_string_new (""); + while (*p) { + switch (*p) { + case ',': + break; + default: + str = g_string_append_c (str, *p); + } + p++; + } + + p = g_strstrip (g_string_free (str, FALSE)); + + return p; +} + +static void +e_category_editor_class_init (ECategoryEditorClass *class) +{ + g_type_class_add_private (class, sizeof (ECategoryEditorPrivate)); +} + +static void +e_category_editor_init (ECategoryEditor *editor) +{ + GtkWidget *dialog_content; + GtkWidget *dialog_action_area; + GtkGrid *grid_category_properties; + GtkWidget *label_name; + GtkWidget *label_icon; + GtkWidget *category_name; + GtkWidget *chooser_button; + GtkWidget *no_image_button; + GtkWidget *chooser_dialog; + GtkWidget *preview; + + editor->priv = E_CATEGORY_EDITOR_GET_PRIVATE (editor); + + chooser_dialog = gtk_file_chooser_dialog_new ( + _("Category Icon"), + NULL, GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, NULL); + + no_image_button = gtk_button_new_with_mnemonic (_("_No Image")); + gtk_button_set_image ( + GTK_BUTTON (no_image_button), + gtk_image_new_from_stock ( + GTK_STOCK_CLOSE, GTK_ICON_SIZE_BUTTON)); + gtk_dialog_add_action_widget ( + GTK_DIALOG (chooser_dialog), + no_image_button, GTK_RESPONSE_NO); + gtk_dialog_add_button ( + GTK_DIALOG (chooser_dialog), + GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT); + gtk_file_chooser_set_local_only ( + GTK_FILE_CHOOSER (chooser_dialog), TRUE); + gtk_widget_show (no_image_button); + + g_signal_connect ( + chooser_dialog, "update-preview", + G_CALLBACK (update_preview), NULL); + + preview = gtk_image_new (); + gtk_file_chooser_set_preview_widget ( + GTK_FILE_CHOOSER (chooser_dialog), preview); + gtk_file_chooser_set_preview_widget_active ( + GTK_FILE_CHOOSER (chooser_dialog), TRUE); + gtk_widget_show_all (preview); + + dialog_content = gtk_dialog_get_content_area (GTK_DIALOG (editor)); + + grid_category_properties = GTK_GRID (gtk_grid_new ()); + gtk_box_pack_start ( + GTK_BOX (dialog_content), + GTK_WIDGET (grid_category_properties), TRUE, TRUE, 0); + gtk_container_set_border_width ( + GTK_CONTAINER (grid_category_properties), 12); + gtk_grid_set_row_spacing (grid_category_properties, 6); + gtk_grid_set_column_spacing (grid_category_properties, 6); + + label_name = gtk_label_new_with_mnemonic (_("Category _Name")); + gtk_widget_set_halign (label_name, GTK_ALIGN_FILL); + gtk_misc_set_alignment (GTK_MISC (label_name), 0, 0.5); + gtk_grid_attach (grid_category_properties, label_name, 0, 0, 1, 1); + + category_name = gtk_entry_new (); + gtk_widget_set_hexpand (category_name, TRUE); + gtk_widget_set_halign (category_name, GTK_ALIGN_FILL); + gtk_label_set_mnemonic_widget (GTK_LABEL (label_name), category_name); + gtk_grid_attach (grid_category_properties, category_name, 1, 0, 1, 1); + editor->priv->category_name = category_name; + + label_icon = gtk_label_new_with_mnemonic (_("Category _Icon")); + gtk_widget_set_halign (label_icon, GTK_ALIGN_FILL); + gtk_misc_set_alignment (GTK_MISC (label_icon), 0, 0.5); + gtk_grid_attach (grid_category_properties, label_icon, 0, 1, 1, 1); + + chooser_button = GTK_WIDGET ( + gtk_file_chooser_button_new_with_dialog (chooser_dialog)); + gtk_widget_set_hexpand (chooser_button, TRUE); + gtk_widget_set_halign (chooser_button, GTK_ALIGN_FILL); + gtk_label_set_mnemonic_widget (GTK_LABEL (label_icon), chooser_button); + gtk_grid_attach (grid_category_properties, chooser_button, 1, 1, 1, 1); + editor->priv->category_icon = chooser_button; + + g_signal_connect ( + chooser_dialog, "response", + G_CALLBACK (file_chooser_response), chooser_button); + + dialog_action_area = gtk_dialog_get_action_area (GTK_DIALOG (editor)); + gtk_button_box_set_layout ( + GTK_BUTTON_BOX (dialog_action_area), GTK_BUTTONBOX_END); + + gtk_dialog_add_buttons ( + GTK_DIALOG (editor), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, NULL); + gtk_dialog_set_default_response (GTK_DIALOG (editor), GTK_RESPONSE_OK); + gtk_window_set_title (GTK_WINDOW (editor), _("Category Properties")); + gtk_window_set_type_hint ( + GTK_WINDOW (editor), GDK_WINDOW_TYPE_HINT_DIALOG); + + gtk_widget_show_all (dialog_content); + + g_signal_connect ( + category_name, "changed", + G_CALLBACK (category_editor_category_name_changed), editor); + + category_editor_category_name_changed ( + GTK_ENTRY (category_name), editor); +} + +/** + * e_categort_editor_new: + * + * Creates a new #ECategoryEditor widget. + * + * Returns: a new #ECategoryEditor + * + * Since: 3.2 + **/ +ECategoryEditor * +e_category_editor_new () +{ + return g_object_new (E_TYPE_CATEGORY_EDITOR, NULL); +} + +/** + * e_category_editor_create_category: + * + * Since: 3.2 + **/ +const gchar * +e_category_editor_create_category (ECategoryEditor *editor) +{ + GtkEntry *entry; + GtkFileChooser *file_chooser; + + g_return_val_if_fail (E_IS_CATEGORY_EDITOR (editor), NULL); + + entry = GTK_ENTRY (editor->priv->category_name); + file_chooser = GTK_FILE_CHOOSER (editor->priv->category_icon); + + do { + const gchar *category_name; + const gchar *correct_category_name; + + if (gtk_dialog_run (GTK_DIALOG (editor)) != GTK_RESPONSE_OK) + return NULL; + + category_name = gtk_entry_get_text (entry); + correct_category_name = check_category_name (category_name); + + if (e_categories_exist (correct_category_name)) { + GtkWidget *error_dialog; + + error_dialog = gtk_message_dialog_new ( + GTK_WINDOW (editor), + 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + _("There is already a category '%s' in the " + "configuration. Please use another name"), + category_name); + + gtk_dialog_run (GTK_DIALOG (error_dialog)); + gtk_widget_destroy (error_dialog); + + /* Now we loop and run the dialog again. */ + + } else { + gchar *category_icon; + + category_icon = + gtk_file_chooser_get_filename (file_chooser); + e_categories_add ( + correct_category_name, NULL, + category_icon, TRUE); + g_free (category_icon); + + return correct_category_name; + } + + } while (TRUE); +} + +/** + * e_category_editor_edit_category: + * + * Since: 3.2 + **/ +gboolean +e_category_editor_edit_category (ECategoryEditor *editor, + const gchar *category) +{ + GtkFileChooser *file_chooser; + const gchar *icon_file; + + g_return_val_if_fail (E_IS_CATEGORY_EDITOR (editor), FALSE); + g_return_val_if_fail (category != NULL, FALSE); + + file_chooser = GTK_FILE_CHOOSER (editor->priv->category_icon); + + gtk_entry_set_text (GTK_ENTRY (editor->priv->category_name), category); + gtk_widget_set_sensitive (editor->priv->category_name, FALSE); + + icon_file = e_categories_get_icon_file_for (category); + if (icon_file) { + gtk_file_chooser_set_filename (file_chooser, icon_file); + update_preview (file_chooser, NULL); + } + + if (gtk_dialog_run (GTK_DIALOG (editor)) == GTK_RESPONSE_OK) { + gchar *category_icon; + + category_icon = gtk_file_chooser_get_filename (file_chooser); + e_categories_set_icon_file_for (category, category_icon); + + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (editor), GTK_RESPONSE_OK, TRUE); + + g_free (category_icon); + + return TRUE; + } + + return FALSE; +} diff --git a/e-util/e-category-editor.h b/e-util/e-category-editor.h new file mode 100644 index 0000000000..bf5ebbe1a8 --- /dev/null +++ b/e-util/e-category-editor.h @@ -0,0 +1,81 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CATEGORY_EDITOR_H +#define E_CATEGORY_EDITOR_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_CATEGORY_EDITOR \ + (e_category_editor_get_type ()) +#define E_CATEGORY_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CATEGORY_EDITOR, ECategoryEditor)) +#define E_CATEGORY_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CATEGORY_EDITOR, ECategoryEditorClass)) +#define E_IS_CATEGORY_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CATEGORY_EDITOR)) +#define E_IS_CATEGORY_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CATEGORY_EDITOR)) +#define E_CATEGORY_EDITOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CATEGORY_EDITOR, ECategoryEditorClass)) + +G_BEGIN_DECLS + +typedef struct _ECategoryEditor ECategoryEditor; +typedef struct _ECategoryEditorClass ECategoryEditorClass; +typedef struct _ECategoryEditorPrivate ECategoryEditorPrivate; + +/** + * ECategoryEditor: + * + * Contains only private data that should be read and manipulated using the + * functions below. + * + * Since: 3.2 + **/ +struct _ECategoryEditor { + GtkDialog parent; + ECategoryEditorPrivate *priv; +}; + +struct _ECategoryEditorClass { + GtkDialogClass parent_class; +}; + +GType e_category_editor_get_type (void); +ECategoryEditor * + e_category_editor_new (void); +const gchar * e_category_editor_create_category + (ECategoryEditor *editor); +gboolean e_category_editor_edit_category (ECategoryEditor *editor, + const gchar *category); + +G_END_DECLS + +#endif /* E_CATEGORY_EDITOR_H */ diff --git a/e-util/e-cell-checkbox.c b/e-util/e-cell-checkbox.c new file mode 100644 index 0000000000..c4340426bd --- /dev/null +++ b/e-util/e-cell-checkbox.c @@ -0,0 +1,102 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-table-item.h" +#include "e-cell-checkbox.h" + +#include "check-empty.xpm" +#include "check-filled.xpm" + +G_DEFINE_TYPE (ECellCheckbox, e_cell_checkbox, E_TYPE_CELL_TOGGLE) + +static GdkPixbuf *checks[2]; + +static void +ecc_print (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height) +{ + cairo_t *cr = gtk_print_context_get_cairo_context (context); + const gint value = GPOINTER_TO_INT ( + e_table_model_value_at ( + ecell_view->e_table_model, model_col, row)); + cairo_save (cr); + + if (value == 1) { + cairo_set_line_width (cr, 2); + cairo_move_to (cr, 3, 11); + cairo_line_to (cr, 7, 14); + cairo_line_to (cr, 11, 5); + cairo_stroke (cr); + } + + cairo_restore (cr); +} + +static void +e_cell_checkbox_class_init (ECellCheckboxClass *class) +{ + ECellClass *ecc = E_CELL_CLASS (class); + + ecc->print = ecc_print; + checks[0] = gdk_pixbuf_new_from_xpm_data (check_empty_xpm); + checks[1] = gdk_pixbuf_new_from_xpm_data (check_filled_xpm); +} + +static void +e_cell_checkbox_init (ECellCheckbox *eccb) +{ + GPtrArray *pixbufs; + + pixbufs = e_cell_toggle_get_pixbufs (E_CELL_TOGGLE (eccb)); + + g_ptr_array_add (pixbufs, g_object_ref (checks[0])); + g_ptr_array_add (pixbufs, g_object_ref (checks[1])); +} + +/** + * e_cell_checkbox_new: + * + * Creates a new ECell renderer that can be used to render check + * boxes. the data provided from the model is cast to an integer. + * zero is used for the off display, and non-zero for checked status. + * + * Returns: an ECell object that can be used to render checkboxes. + */ +ECell * +e_cell_checkbox_new (void) +{ + return g_object_new (E_TYPE_CELL_CHECKBOX, NULL); +} diff --git a/e-util/e-cell-checkbox.h b/e-util/e-cell-checkbox.h new file mode 100644 index 0000000000..2d1d9db053 --- /dev/null +++ b/e-util/e-cell-checkbox.h @@ -0,0 +1,71 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_CHECKBOX_H_ +#define _E_CELL_CHECKBOX_H_ + +#include <e-util/e-cell-toggle.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_CHECKBOX \ + (e_cell_checkbox_get_type ()) +#define E_CELL_CHECKBOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_CHECKBOX, ECellCheckbox)) +#define E_CELL_CHECKBOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_CHECKBOX, ECellCheckboxClass)) +#define E_IS_CELL_CHECKBOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_CHECKBOX)) +#define E_IS_CELL_CHECKBOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_CHECKBOX)) +#define E_CELL_CHECKBOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_CHECKBOX, ECellCheckboxClass)) + +G_BEGIN_DECLS + +typedef struct _ECellCheckbox ECellCheckbox; +typedef struct _ECellCheckboxClass ECellCheckboxClass; + +struct _ECellCheckbox { + ECellToggle parent; +}; + +struct _ECellCheckboxClass { + ECellToggleClass parent_class; +}; + +GType e_cell_checkbox_get_type (void) G_GNUC_CONST; +ECell * e_cell_checkbox_new (void); + +G_END_DECLS + +#endif /* _E_CELL_CHECKBOX_H_ */ + diff --git a/e-util/e-cell-combo.c b/e-util/e-cell-combo.c new file mode 100644 index 0000000000..dba6b53c50 --- /dev/null +++ b/e-util/e-cell-combo.c @@ -0,0 +1,838 @@ +/* + * e-cell-combo.c: Combo cell renderer + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * ECellCombo - a subclass of ECellPopup used to support popup lists like a + * GtkCombo widget. It only supports a basic popup list of strings at present, + * with no auto-completion. + */ + +/* + * Notes: (handling pointer grabs and GTK+ grabs is a nightmare!) + * + * o We must grab the pointer when we show the popup, so that if any buttons + * are pressed outside the application we hide the popup. + * + * o We have to be careful when popping up any widgets which also grab the + * pointer at some point, since we will lose our own pointer grab. + * When we pop up a list it will grab the pointer itself when an item is + * selected, and release the grab when the button is released. + * Fortunately we hide the popup at this point, so it isn't a problem. + * But for other types of widgets in the popup it could cause trouble. + * - I think GTK+ should provide help for this (nested pointer grabs?). + * + * o We must set the 'owner_events' flag of the pointer grab to TRUE so that + * pointer events get reported to all the application windows as normal. + * If we don't do this then the widgets in the popup may not work properly. + * + * o We must do a gtk_grab_add() so that we only allow events to go to the + * widgets within the popup (though some special events still get reported + * to the widget owning the window). Doing th gtk_grab_add() on the toplevel + * popup window should be fine. We can then check for any events that should + * close the popup, like the Escape key, or a button press outside the popup. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-cell-combo.h" + +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include "e-cell-text.h" +#include "e-table-item.h" +#include "e-unicode.h" + +#define d(x) + +/* The height to make the popup list if there aren't any items in it. */ +#define E_CELL_COMBO_LIST_EMPTY_HEIGHT 15 + +static void e_cell_combo_dispose (GObject *object); +static gint e_cell_combo_do_popup (ECellPopup *ecp, + GdkEvent *event, + gint row, + gint view_col); +static void e_cell_combo_select_matching_item + (ECellCombo *ecc); +static void e_cell_combo_show_popup (ECellCombo *ecc, + gint row, + gint view_col); +static void e_cell_combo_get_popup_pos (ECellCombo *ecc, + gint row, + gint view_col, + gint *x, + gint *y, + gint *height, + gint *width); +static void e_cell_combo_selection_changed (GtkTreeSelection *selection, + ECellCombo *ecc); +static gint e_cell_combo_button_press (GtkWidget *popup_window, + GdkEvent *button_event, + ECellCombo *ecc); +static gint e_cell_combo_button_release (GtkWidget *popup_window, + GdkEvent *button_event, + ECellCombo *ecc); +static gint e_cell_combo_key_press (GtkWidget *popup_window, + GdkEvent *key_event, + ECellCombo *ecc); +static void e_cell_combo_update_cell (ECellCombo *ecc); +static void e_cell_combo_restart_edit (ECellCombo *ecc); + +G_DEFINE_TYPE (ECellCombo, e_cell_combo, E_TYPE_CELL_POPUP) + +static void +e_cell_combo_class_init (ECellComboClass *class) +{ + ECellPopupClass *ecpc = E_CELL_POPUP_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = e_cell_combo_dispose; + + ecpc->popup = e_cell_combo_do_popup; +} + +static void +e_cell_combo_init (ECellCombo *ecc) +{ + GtkWidget *frame; + AtkObject *a11y; + GtkListStore *store; + GtkTreeSelection *selection; + GtkScrolledWindow *scrolled_window; + + /* We create one popup window for the ECell, since there will only + * ever be one popup in use at a time. */ + ecc->popup_window = gtk_window_new (GTK_WINDOW_POPUP); + + gtk_window_set_type_hint ( + GTK_WINDOW (ecc->popup_window), GDK_WINDOW_TYPE_HINT_COMBO); + gtk_window_set_resizable (GTK_WINDOW (ecc->popup_window), TRUE); + + frame = gtk_frame_new (NULL); + gtk_container_add (GTK_CONTAINER (ecc->popup_window), frame); + gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_OUT); + gtk_widget_show (frame); + + ecc->popup_scrolled_window = gtk_scrolled_window_new (NULL, NULL); + scrolled_window = GTK_SCROLLED_WINDOW (ecc->popup_scrolled_window); + + gtk_scrolled_window_set_policy ( + scrolled_window, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_widget_set_can_focus ( + gtk_scrolled_window_get_hscrollbar (scrolled_window), FALSE); + gtk_widget_set_can_focus ( + gtk_scrolled_window_get_vscrollbar (scrolled_window), FALSE); + gtk_container_add (GTK_CONTAINER (frame), ecc->popup_scrolled_window); + gtk_widget_show (ecc->popup_scrolled_window); + + store = gtk_list_store_new (1, G_TYPE_STRING); + ecc->popup_tree_view = + gtk_tree_view_new_with_model (GTK_TREE_MODEL (store)); + g_object_unref (store); + + gtk_tree_view_append_column ( + GTK_TREE_VIEW (ecc->popup_tree_view), + gtk_tree_view_column_new_with_attributes ( + "Text", gtk_cell_renderer_text_new (), + "text", 0, NULL)); + + gtk_tree_view_set_headers_visible ( + GTK_TREE_VIEW (ecc->popup_tree_view), FALSE); + + selection = gtk_tree_view_get_selection ( + GTK_TREE_VIEW (ecc->popup_tree_view)); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); + gtk_scrolled_window_add_with_viewport ( + GTK_SCROLLED_WINDOW (ecc->popup_scrolled_window), + ecc->popup_tree_view); + gtk_container_set_focus_vadjustment ( + GTK_CONTAINER (ecc->popup_tree_view), + gtk_scrolled_window_get_vadjustment ( + GTK_SCROLLED_WINDOW (ecc->popup_scrolled_window))); + gtk_container_set_focus_hadjustment ( + GTK_CONTAINER (ecc->popup_tree_view), + gtk_scrolled_window_get_hadjustment ( + GTK_SCROLLED_WINDOW (ecc->popup_scrolled_window))); + gtk_widget_show (ecc->popup_tree_view); + + a11y = gtk_widget_get_accessible (ecc->popup_tree_view); + atk_object_set_name (a11y, _("popup list")); + + g_signal_connect ( + selection, "changed", + G_CALLBACK (e_cell_combo_selection_changed), ecc); + g_signal_connect ( + ecc->popup_window, "button_press_event", + G_CALLBACK (e_cell_combo_button_press), ecc); + g_signal_connect ( + ecc->popup_window, "button_release_event", + G_CALLBACK (e_cell_combo_button_release), ecc); + g_signal_connect ( + ecc->popup_window, "key_press_event", + G_CALLBACK (e_cell_combo_key_press), ecc); +} + +/** + * e_cell_combo_new: + * + * Creates a new ECellCombo renderer. + * + * Returns: an ECellCombo object. + */ +ECell * +e_cell_combo_new (void) +{ + return g_object_new (E_TYPE_CELL_COMBO, NULL); +} + +/* + * GObject::dispose method + */ +static void +e_cell_combo_dispose (GObject *object) +{ + ECellCombo *ecc = E_CELL_COMBO (object); + + if (ecc->popup_window != NULL) { + gtk_widget_destroy (ecc->popup_window); + ecc->popup_window = NULL; + } + + if (ecc->grabbed_keyboard != NULL) { + gdk_device_ungrab (ecc->grabbed_keyboard, GDK_CURRENT_TIME); + g_object_unref (ecc->grabbed_keyboard); + ecc->grabbed_keyboard = NULL; + } + + if (ecc->grabbed_pointer != NULL) { + gdk_device_ungrab (ecc->grabbed_pointer, GDK_CURRENT_TIME); + g_object_unref (ecc->grabbed_pointer); + ecc->grabbed_pointer = NULL; + } + + G_OBJECT_CLASS (e_cell_combo_parent_class)->dispose (object); +} + +void +e_cell_combo_set_popdown_strings (ECellCombo *ecc, + GList *strings) +{ + GList *elem; + GtkListStore *store; + + g_return_if_fail (E_IS_CELL_COMBO (ecc)); + g_return_if_fail (strings != NULL); + + store = GTK_LIST_STORE ( + gtk_tree_view_get_model ( + GTK_TREE_VIEW (ecc->popup_tree_view))); + gtk_list_store_clear (store); + + for (elem = strings; elem; elem = elem->next) { + GtkTreeIter iter; + gchar *utf8_text = elem->data; + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, utf8_text, -1); + } +} + +static gint +e_cell_combo_do_popup (ECellPopup *ecp, + GdkEvent *event, + gint row, + gint view_col) +{ + ECellCombo *ecc = E_CELL_COMBO (ecp); + GtkTreeSelection *selection; + GdkGrabStatus grab_status; + GdkWindow *window; + GdkDevice *keyboard; + GdkDevice *pointer; + GdkDevice *event_device; + guint32 event_time; + + g_return_val_if_fail (ecc->grabbed_keyboard == NULL, FALSE); + g_return_val_if_fail (ecc->grabbed_pointer == NULL, FALSE); + + selection = gtk_tree_view_get_selection ( + GTK_TREE_VIEW (ecc->popup_tree_view)); + + g_signal_handlers_block_by_func ( + selection, e_cell_combo_selection_changed, ecc); + + e_cell_combo_show_popup (ecc, row, view_col); + e_cell_combo_select_matching_item (ecc); + + g_signal_handlers_unblock_by_func ( + selection, e_cell_combo_selection_changed, ecc); + + window = gtk_widget_get_window (ecc->popup_tree_view); + + event_device = gdk_event_get_device (event); + event_time = gdk_event_get_time (event); + + if (gdk_device_get_source (event_device) == GDK_SOURCE_KEYBOARD) { + keyboard = event_device; + pointer = gdk_device_get_associated_device (event_device); + } else { + keyboard = gdk_device_get_associated_device (event_device); + pointer = event_device; + } + + if (pointer != NULL) { + grab_status = gdk_device_grab ( + pointer, + window, + GDK_OWNERSHIP_NONE, + TRUE, + GDK_ENTER_NOTIFY_MASK | + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_HINT_MASK | + GDK_BUTTON1_MOTION_MASK, + NULL, + event_time); + + if (grab_status != GDK_GRAB_SUCCESS) + return FALSE; + + ecc->grabbed_pointer = g_object_ref (pointer); + } + + gtk_grab_add (ecc->popup_window); + + if (keyboard != NULL) { + grab_status = gdk_device_grab ( + keyboard, + window, + GDK_OWNERSHIP_NONE, + TRUE, + GDK_KEY_PRESS_MASK | + GDK_KEY_RELEASE_MASK, + NULL, + event_time); + + if (grab_status != GDK_GRAB_SUCCESS) { + if (ecc->grabbed_pointer != NULL) { + gdk_device_ungrab ( + ecc->grabbed_pointer, + event_time); + g_object_unref (ecc->grabbed_pointer); + ecc->grabbed_pointer = NULL; + } + return FALSE; + } + + ecc->grabbed_keyboard = g_object_ref (keyboard); + } + + return TRUE; +} + +static void +e_cell_combo_select_matching_item (ECellCombo *ecc) +{ + ECellPopup *ecp = E_CELL_POPUP (ecc); + ECellView *ecv = (ECellView *) ecp->popup_cell_view; + ECellText *ecell_text = E_CELL_TEXT (ecp->child); + ETableItem *eti; + ETableCol *ecol; + gboolean found = FALSE; + gchar *cell_text; + gboolean valid; + GtkTreeSelection *selection; + GtkTreeIter iter; + GtkTreeModel *model; + + eti = E_TABLE_ITEM (ecp->popup_cell_view->cell_view.e_table_item_view); + + ecol = e_table_header_get_column (eti->header, ecp->popup_view_col); + cell_text = e_cell_text_get_text ( + ecell_text, ecv->e_table_model, + ecol->col_idx, ecp->popup_row); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (ecc->popup_tree_view)); + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ecc->popup_tree_view)); + + for (valid = gtk_tree_model_get_iter_first (model, &iter); + valid && !found; + valid = gtk_tree_model_iter_next (model, &iter)) { + gchar *str = NULL; + + gtk_tree_model_get (model, &iter, 0, &str, -1); + + if (str && g_str_equal (str, cell_text)) { + GtkTreePath *path; + + path = gtk_tree_model_get_path (model, &iter); + gtk_tree_view_set_cursor ( + GTK_TREE_VIEW (ecc->popup_tree_view), + path, NULL, FALSE); + gtk_tree_path_free (path); + + found = TRUE; + } + + g_free (str); + } + + if (!found) + gtk_tree_selection_unselect_all (selection); + + e_cell_text_free_text (ecell_text, cell_text); +} + +static void +e_cell_combo_show_popup (ECellCombo *ecc, + gint row, + gint view_col) +{ + GdkWindow *window; + GtkAllocation allocation; + gint x, y, width, height, old_width, old_height; + + gtk_widget_get_allocation (ecc->popup_window, &allocation); + + /* This code is practically copied from GtkCombo. */ + old_width = allocation.width; + old_height = allocation.height; + + e_cell_combo_get_popup_pos (ecc, row, view_col, &x, &y, &height, &width); + + /* workaround for gtk_scrolled_window_size_allocate bug */ + if (old_width != width || old_height != height) { + gtk_widget_hide ( + gtk_scrolled_window_get_hscrollbar ( + GTK_SCROLLED_WINDOW (ecc->popup_scrolled_window))); + gtk_widget_hide ( + gtk_scrolled_window_get_vscrollbar ( + GTK_SCROLLED_WINDOW (ecc->popup_scrolled_window))); + } + + gtk_window_move (GTK_WINDOW (ecc->popup_window), x, y); + gtk_widget_set_size_request (ecc->popup_window, width, height); + gtk_widget_realize (ecc->popup_window); + window = gtk_widget_get_window (ecc->popup_window); + gdk_window_resize (window, width, height); + gtk_widget_show (ecc->popup_window); + + e_cell_popup_set_shown (E_CELL_POPUP (ecc), TRUE); + d (g_print ("%s: popup_shown = TRUE\n", __FUNCTION__)); +} + +/* Calculates the size and position of the popup window (like GtkCombo). */ +static void +e_cell_combo_get_popup_pos (ECellCombo *ecc, + gint row, + gint view_col, + gint *x, + gint *y, + gint *height, + gint *width) +{ + ECellPopup *ecp = E_CELL_POPUP (ecc); + ETableItem *eti; + GtkWidget *canvas; + GtkWidget *widget; + GtkWidget *popwin_child; + GtkWidget *popup_child; + GtkStyle *popwin_style; + GtkStyle *popup_style; + GdkWindow *window; + GtkBin *popwin; + GtkScrolledWindow *popup; + GtkRequisition requisition; + GtkRequisition list_requisition; + gboolean show_vscroll = FALSE, show_hscroll = FALSE; + gint avail_height, avail_width, min_height, work_height, screen_width; + gint column_width, row_height, scrollbar_width; + gdouble x1, y1; + gdouble wx, wy; + + eti = E_TABLE_ITEM (ecp->popup_cell_view->cell_view.e_table_item_view); + canvas = GTK_WIDGET (GNOME_CANVAS_ITEM (eti)->canvas); + + /* This code is practically copied from GtkCombo. */ + popup = GTK_SCROLLED_WINDOW (ecc->popup_scrolled_window); + popwin = GTK_BIN (ecc->popup_window); + + window = gtk_widget_get_window (canvas); + gdk_window_get_origin (window, x, y); + + x1 = e_table_header_col_diff (eti->header, 0, view_col + 1); + y1 = e_table_item_row_diff (eti, 0, row + 1); + column_width = e_table_header_col_diff ( + eti->header, view_col, view_col + 1); + row_height = e_table_item_row_diff (eti, row, row + 1); + gnome_canvas_item_i2w (GNOME_CANVAS_ITEM (eti), &x1, &y1); + + gnome_canvas_world_to_window ( + GNOME_CANVAS (canvas), x1, y1, &wx, &wy); + + x1 = wx; + y1 = wy; + + *x += x1; + /* The ETable positions don't include the grid lines, I think, so we add 1. */ + *y += y1 + 1 - (gint) + gtk_adjustment_get_value ( + gtk_scrollable_get_vadjustment ( + GTK_SCROLLABLE (&((GnomeCanvas *) canvas)->layout))) + + ((GnomeCanvas *) canvas)->zoom_yofs; + + widget = gtk_scrolled_window_get_vscrollbar (popup); + gtk_widget_get_preferred_size (widget, &requisition, NULL); + + scrollbar_width = + requisition.width + + GTK_SCROLLED_WINDOW_CLASS (G_OBJECT_GET_CLASS (popup))->scrollbar_spacing; + + avail_height = gdk_screen_height () - *y; + + /* We'll use the entire screen width if needed, but we save space for + * the vertical scrollbar in case we need to show that. */ + screen_width = gdk_screen_width (); + avail_width = screen_width - scrollbar_width; + + widget = gtk_scrolled_window_get_vscrollbar (popup); + gtk_widget_get_preferred_size (widget, &requisition, NULL); + + gtk_widget_get_preferred_size (ecc->popup_tree_view, &list_requisition, NULL); + min_height = MIN (list_requisition.height, requisition.height); + if (!gtk_tree_model_iter_n_children ( + gtk_tree_view_get_model ( + GTK_TREE_VIEW (ecc->popup_tree_view)), NULL)) + list_requisition.height += E_CELL_COMBO_LIST_EMPTY_HEIGHT; + + popwin_child = gtk_bin_get_child (popwin); + popwin_style = gtk_widget_get_style (popwin_child); + + popup_child = gtk_bin_get_child (GTK_BIN (popup)); + popup_style = gtk_widget_get_style (popup_child); + + /* Calculate the desired width. */ + *width = list_requisition.width + + 2 * popwin_style->xthickness + + 2 * gtk_container_get_border_width (GTK_CONTAINER (popwin_child)) + + 2 * gtk_container_get_border_width (GTK_CONTAINER (popup)) + + 2 * gtk_container_get_border_width (GTK_CONTAINER (popup_child)) + + 2 * popup_style->xthickness; + + /* Use at least the same width as the column. */ + if (*width < column_width) + *width = column_width; + + /* If it is larger than the available width, use that instead and show + * the horizontal scrollbar. */ + if (*width > avail_width) { + *width = avail_width; + show_hscroll = TRUE; + } + + /* Calculate all the borders etc. that we need to add to the height. */ + work_height = (2 * popwin_style->ythickness + + 2 * gtk_container_get_border_width (GTK_CONTAINER (popwin_child)) + + 2 * gtk_container_get_border_width (GTK_CONTAINER (popup)) + + 2 * gtk_container_get_border_width (GTK_CONTAINER (popup_child)) + + 2 * popup_style->xthickness); + + widget = gtk_scrolled_window_get_hscrollbar (popup); + gtk_widget_get_preferred_size (widget, &requisition, NULL); + + /* Add on the height of the horizontal scrollbar if we need it. */ + if (show_hscroll) + work_height += + requisition.height + + GTK_SCROLLED_WINDOW_GET_CLASS (popup)->scrollbar_spacing; + + /* Check if it fits in the available height. */ + if (work_height + list_requisition.height > avail_height) { + /* It doesn't fit, so we see if we have the minimum space + * needed. */ + if (work_height + min_height > avail_height + && *y - row_height > avail_height) { + /* We don't, so we show the popup above the cell + * instead of below it. */ + avail_height = *y - row_height; + *y -= (work_height + list_requisition.height + + row_height); + if (*y < 0) + *y = 0; + } + } + + /* Check if we still need the vertical scrollbar. */ + if (work_height + list_requisition.height > avail_height) { + *width += scrollbar_width; + show_vscroll = TRUE; + } + + /* We try to line it up with the right edge of the column, but we don't + * want it to go off the edges of the screen. */ + if (*x > screen_width) + *x = screen_width; + *x -= *width; + if (*x < 0) + *x = 0; + + if (show_vscroll) + *height = avail_height; + else + *height = work_height + list_requisition.height; +} + +static void +e_cell_combo_selection_changed (GtkTreeSelection *selection, + ECellCombo *ecc) +{ + GtkTreeIter iter; + GtkTreeModel *model; + + if (!gtk_widget_get_realized (ecc->popup_window) || + !gtk_tree_selection_get_selected (selection, &model, &iter)) + return; + + e_cell_combo_update_cell (ecc); + e_cell_combo_restart_edit (ecc); +} + +/* This handles button press events in the popup window. + * Note that since we have a pointer grab on this window, we also get button + * press events for windows outside the application here, so we hide the popup + * window if that happens. We also get propagated events from child widgets + * which we ignore. */ +static gint +e_cell_combo_button_press (GtkWidget *popup_window, + GdkEvent *button_event, + ECellCombo *ecc) +{ + GtkWidget *event_widget; + guint32 event_time; + + event_time = gdk_event_get_time (button_event); + event_widget = gtk_get_event_widget (button_event); + + /* If the button press was for a widget inside the popup list, but + * not the popup window itself, then we ignore the event and return + * FALSE. Otherwise we will hide the popup. + * Note that since we have a pointer grab on the popup list, button + * presses outside the application will be reported to this window, + * which is why we hide the popup in this case. */ + while (event_widget) { + event_widget = gtk_widget_get_parent (event_widget); + if (event_widget == ecc->popup_tree_view) + return FALSE; + } + + gtk_grab_remove (ecc->popup_window); + + if (ecc->grabbed_keyboard != NULL) { + gdk_device_ungrab (ecc->grabbed_keyboard, event_time); + g_object_unref (ecc->grabbed_keyboard); + ecc->grabbed_keyboard = NULL; + } + + if (ecc->grabbed_pointer != NULL) { + gdk_device_ungrab (ecc->grabbed_pointer, event_time); + g_object_unref (ecc->grabbed_pointer); + ecc->grabbed_pointer = NULL; + } + + gtk_widget_hide (ecc->popup_window); + + e_cell_popup_set_shown (E_CELL_POPUP (ecc), FALSE); + d (g_print ("%s: popup_shown = FALSE\n", __FUNCTION__)); + + /* We don't want to update the cell here. Since the list is in browse + * mode there will always be one item selected, so when we popup the + * list one item is selected even if it doesn't match the current text + * in the cell. So if you click outside the popup (which is what has + * happened here) it is better to not update the cell. */ + /*e_cell_combo_update_cell (ecc);*/ + e_cell_combo_restart_edit (ecc); + + return TRUE; +} + +/* This handles button release events in the popup window. If the button is + * released inside the list, we want to hide the popup window and update the + * cell with the new selection. */ +static gint +e_cell_combo_button_release (GtkWidget *popup_window, + GdkEvent *button_event, + ECellCombo *ecc) +{ + GtkWidget *event_widget; + guint32 event_time; + + event_time = gdk_event_get_time (button_event); + event_widget = gtk_get_event_widget (button_event); + + /* See if the button was released in the list (or its children). */ + while (event_widget && event_widget != ecc->popup_tree_view) + event_widget = gtk_widget_get_parent (event_widget); + + /* If it wasn't, then we just ignore the event. */ + if (event_widget != ecc->popup_tree_view) + return FALSE; + + /* The button was released inside the list, so we hide the popup and + * update the cell to reflect the new selection. */ + + gtk_grab_remove (ecc->popup_window); + + if (ecc->grabbed_keyboard != NULL) { + gdk_device_ungrab (ecc->grabbed_keyboard, event_time); + g_object_unref (ecc->grabbed_keyboard); + ecc->grabbed_keyboard = NULL; + } + + if (ecc->grabbed_pointer != NULL) { + gdk_device_ungrab (ecc->grabbed_pointer, event_time); + g_object_unref (ecc->grabbed_pointer); + ecc->grabbed_pointer = NULL; + } + + gtk_widget_hide (ecc->popup_window); + + e_cell_popup_set_shown (E_CELL_POPUP (ecc), FALSE); + d (g_print ("%s: popup_shown = FALSE\n", __FUNCTION__)); + + e_cell_combo_update_cell (ecc); + e_cell_combo_restart_edit (ecc); + + return TRUE; +} + +/* This handles key press events in the popup window. If the Escape key is + * pressed we hide the popup, and do not change the cell contents. */ +static gint +e_cell_combo_key_press (GtkWidget *popup_window, + GdkEvent *key_event, + ECellCombo *ecc) +{ + guint event_keyval = 0; + guint32 event_time; + + gdk_event_get_keyval (key_event, &event_keyval); + event_time = gdk_event_get_time (key_event); + + /* If the Escape key is pressed we hide the popup. */ + if (event_keyval != GDK_KEY_Escape + && event_keyval != GDK_KEY_Return + && event_keyval != GDK_KEY_KP_Enter + && event_keyval != GDK_KEY_ISO_Enter + && event_keyval != GDK_KEY_3270_Enter) + return FALSE; + + if (event_keyval == GDK_KEY_Escape && + (!ecc->popup_window || !gtk_widget_get_visible (ecc->popup_window))) + return FALSE; + + gtk_grab_remove (ecc->popup_window); + + if (ecc->grabbed_keyboard != NULL) { + gdk_device_ungrab (ecc->grabbed_keyboard, event_time); + g_object_unref (ecc->grabbed_keyboard); + ecc->grabbed_keyboard = NULL; + } + + if (ecc->grabbed_pointer != NULL) { + gdk_device_ungrab (ecc->grabbed_pointer, event_time); + g_object_unref (ecc->grabbed_pointer); + ecc->grabbed_pointer = NULL; + } + + gtk_widget_hide (ecc->popup_window); + + e_cell_popup_set_shown (E_CELL_POPUP (ecc), FALSE); + d (g_print ("%s: popup_shown = FALSE\n", __FUNCTION__)); + + if (event_keyval != GDK_KEY_Escape) + e_cell_combo_update_cell (ecc); + + e_cell_combo_restart_edit (ecc); + + return TRUE; +} + +static void +e_cell_combo_update_cell (ECellCombo *ecc) +{ + ECellPopup *ecp = E_CELL_POPUP (ecc); + ECellView *ecv = (ECellView *) ecp->popup_cell_view; + ECellText *ecell_text = E_CELL_TEXT (ecp->child); + ETableItem *eti = E_TABLE_ITEM (ecv->e_table_item_view); + ETableCol *ecol; + GtkTreeSelection *selection = gtk_tree_view_get_selection ( + GTK_TREE_VIEW (ecc->popup_tree_view)); + GtkTreeModel *model; + GtkTreeIter iter; + gchar *text = NULL, *old_text; + + /* Return if no item is selected. */ + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) + return; + + /* Get the text of the selected item. */ + gtk_tree_model_get (model, &iter, 0, &text, -1); + g_return_if_fail (text != NULL); + + /* Compare it with the existing cell contents. */ + ecol = e_table_header_get_column (eti->header, ecp->popup_view_col); + + old_text = e_cell_text_get_text ( + ecell_text, ecv->e_table_model, + ecol->col_idx, ecp->popup_row); + + /* If they are different, update the cell contents. */ + if (old_text && strcmp (old_text, text)) { + e_cell_text_set_value ( + ecell_text, ecv->e_table_model, + ecol->col_idx, ecp->popup_row, text); + } + + e_cell_text_free_text (ecell_text, old_text); + g_free (text); +} + +static void +e_cell_combo_restart_edit (ECellCombo *ecc) +{ + /* This doesn't work. ETable stops the edit straight-away again. */ +#if 0 + ECellView *ecv = (ECellView *) ecc->popup_cell_view; + ETableItem *eti = E_TABLE_ITEM (ecv->e_table_item_view); + + e_table_item_enter_edit (eti, ecc->popup_view_col, ecc->popup_row); +#endif +} + diff --git a/e-util/e-cell-combo.h b/e-util/e-cell-combo.h new file mode 100644 index 0000000000..04f17c60fa --- /dev/null +++ b/e-util/e-cell-combo.h @@ -0,0 +1,89 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * ECellCombo - a subclass of ECellPopup used to support popup lists like a + * GtkCombo widget. It only supports a basic popup list of strings at present, + * with no auto-completion. The child ECell of the ECellPopup must be an + * ECellText or subclass. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_COMBO_H_ +#define _E_CELL_COMBO_H_ + +#include <e-util/e-cell-popup.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_COMBO \ + (e_cell_combo_get_type ()) +#define E_CELL_COMBO(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_COMBO, ECellCombo)) +#define E_CELL_COMBO_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_COMBO, ECellComboClass)) +#define E_IS_CELL_COMBO(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_COMBO)) +#define E_IS_CELL_COMBO_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_COMBO)) +#define E_CELL_COMBO_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_COMBO, ECellComboClass)) + +G_BEGIN_DECLS + +typedef struct _ECellCombo ECellCombo; +typedef struct _ECellComboClass ECellComboClass; + +struct _ECellCombo { + ECellPopup parent; + + GtkWidget *popup_window; + GtkWidget *popup_scrolled_window; + GtkWidget *popup_tree_view; + + GdkDevice *grabbed_keyboard; + GdkDevice *grabbed_pointer; +}; + +struct _ECellComboClass { + ECellPopupClass parent_class; +}; + +GType e_cell_combo_get_type (void) G_GNUC_CONST; +ECell * e_cell_combo_new (void); + +/* These must be UTF-8. */ +void e_cell_combo_set_popdown_strings + (ECellCombo *ecc, + GList *strings); + +G_END_DECLS + +#endif /* _E_CELL_COMBO_H_ */ diff --git a/e-util/e-cell-date-edit.c b/e-util/e-cell-date-edit.c new file mode 100644 index 0000000000..4f35fbb266 --- /dev/null +++ b/e-util/e-cell-date-edit.c @@ -0,0 +1,1039 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * ECellDateEdit - a subclass of ECellPopup used to show a date with a popup + * window to edit it. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-cell-date-edit.h" + +#include <string.h> +#include <time.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include <libedataserver/libedataserver.h> + +#include "e-calendar.h" +#include "e-cell-text.h" +#include "e-table-item.h" + +static void e_cell_date_edit_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); +static void e_cell_date_edit_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); +static void e_cell_date_edit_dispose (GObject *object); + +static gint e_cell_date_edit_do_popup (ECellPopup *ecp, + GdkEvent *event, + gint row, + gint view_col); +static void e_cell_date_edit_set_popup_values (ECellDateEdit *ecde); +static void e_cell_date_edit_select_matching_time (ECellDateEdit *ecde, + gchar *time); +static void e_cell_date_edit_show_popup (ECellDateEdit *ecde, + gint row, + gint view_col); +static void e_cell_date_edit_get_popup_pos (ECellDateEdit *ecde, + gint row, + gint view_col, + gint *x, + gint *y, + gint *height, + gint *width); + +static void e_cell_date_edit_rebuild_time_list (ECellDateEdit *ecde); + +static gint e_cell_date_edit_key_press (GtkWidget *popup_window, + GdkEventKey *event, + ECellDateEdit *ecde); +static gint e_cell_date_edit_button_press (GtkWidget *popup_window, + GdkEvent *button_event, + ECellDateEdit *ecde); +static void e_cell_date_edit_on_ok_clicked (GtkWidget *button, + ECellDateEdit *ecde); +static void e_cell_date_edit_show_time_invalid_warning (ECellDateEdit *ecde); +static void e_cell_date_edit_on_now_clicked (GtkWidget *button, + ECellDateEdit *ecde); +static void e_cell_date_edit_on_none_clicked (GtkWidget *button, + ECellDateEdit *ecde); +static void e_cell_date_edit_on_today_clicked (GtkWidget *button, + ECellDateEdit *ecde); +static void e_cell_date_edit_update_cell (ECellDateEdit *ecde, + const gchar *text); +static void e_cell_date_edit_on_time_selected (GtkTreeSelection *selection, + ECellDateEdit *ecde); +static void e_cell_date_edit_hide_popup (ECellDateEdit *ecde); + +/* Our arguments. */ +enum { + PROP_0, + PROP_SHOW_TIME, + PROP_SHOW_NOW_BUTTON, + PROP_SHOW_TODAY_BUTTON, + PROP_ALLOW_NO_DATE_SET, + PROP_USE_24_HOUR_FORMAT, + PROP_LOWER_HOUR, + PROP_UPPER_HOUR +}; + +G_DEFINE_TYPE (ECellDateEdit, e_cell_date_edit, E_TYPE_CELL_POPUP) + +static void +e_cell_date_edit_class_init (ECellDateEditClass *class) +{ + GObjectClass *object_class; + ECellPopupClass *ecpc; + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = e_cell_date_edit_get_property; + object_class->set_property = e_cell_date_edit_set_property; + object_class->dispose = e_cell_date_edit_dispose; + + ecpc = E_CELL_POPUP_CLASS (class); + ecpc->popup = e_cell_date_edit_do_popup; + + g_object_class_install_property ( + object_class, + PROP_SHOW_TIME, + g_param_spec_boolean ( + "show_time", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_NOW_BUTTON, + g_param_spec_boolean ( + "show_now_button", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_TODAY_BUTTON, + g_param_spec_boolean ( + "show_today_button", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_ALLOW_NO_DATE_SET, + g_param_spec_boolean ( + "allow_no_date_set", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_USE_24_HOUR_FORMAT, + g_param_spec_boolean ( + "use_24_hour_format", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_LOWER_HOUR, + g_param_spec_int ( + "lower_hour", + NULL, + NULL, + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_UPPER_HOUR, + g_param_spec_int ( + "upper_hour", + NULL, + NULL, + G_MININT, + G_MAXINT, + 24, + G_PARAM_READWRITE)); +} + +static void +e_cell_date_edit_init (ECellDateEdit *ecde) +{ + GtkWidget *frame, *vbox, *hbox, *vbox2; + GtkWidget *scrolled_window, *bbox, *tree_view; + GtkWidget *now_button, *today_button, *none_button, *ok_button; + GtkListStore *store; + + ecde->lower_hour = 0; + ecde->upper_hour = 24; + ecde->use_24_hour_format = TRUE; + ecde->need_time_list_rebuild = TRUE; + ecde->freeze_count = 0; + ecde->time_callback = NULL; + ecde->time_callback_data = NULL; + ecde->time_callback_destroy = NULL; + + /* We create one popup window for the ECell, since there will only + * ever be one popup in use at a time. */ + ecde->popup_window = gtk_window_new (GTK_WINDOW_POPUP); + + gtk_window_set_type_hint ( + GTK_WINDOW (ecde->popup_window), + GDK_WINDOW_TYPE_HINT_COMBO); + gtk_window_set_resizable (GTK_WINDOW (ecde->popup_window), TRUE); + + frame = gtk_frame_new (NULL); + gtk_container_add (GTK_CONTAINER (ecde->popup_window), frame); + gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_OUT); + gtk_widget_show (frame); + + vbox = gtk_vbox_new (FALSE, 0); + gtk_container_add (GTK_CONTAINER (frame), vbox); + gtk_widget_show (vbox); + + hbox = gtk_hbox_new (FALSE, 4); + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + gtk_widget_show (hbox); + + ecde->calendar = e_calendar_new (); + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (E_CALENDAR (ecde->calendar)->calitem), + "move_selection_when_moving", FALSE, + NULL); + gtk_box_pack_start (GTK_BOX (hbox), ecde->calendar, TRUE, TRUE, 0); + gtk_widget_show (ecde->calendar); + + vbox2 = gtk_vbox_new (FALSE, 2); + gtk_box_pack_start (GTK_BOX (hbox), vbox2, TRUE, TRUE, 0); + gtk_widget_show (vbox2); + + ecde->time_entry = gtk_entry_new (); + gtk_widget_set_size_request (ecde->time_entry, 50, -1); + gtk_box_pack_start ( + GTK_BOX (vbox2), ecde->time_entry, + FALSE, FALSE, 0); + gtk_widget_show (ecde->time_entry); + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_box_pack_start (GTK_BOX (vbox2), scrolled_window, TRUE, TRUE, 0); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_NEVER, GTK_POLICY_ALWAYS); + gtk_widget_show (scrolled_window); + + store = gtk_list_store_new (1, G_TYPE_STRING); + tree_view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store)); + g_object_unref (store); + + gtk_tree_view_append_column ( + GTK_TREE_VIEW (tree_view), + gtk_tree_view_column_new_with_attributes ( + "Text", gtk_cell_renderer_text_new (), "text", 0, NULL)); + + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree_view), FALSE); + + gtk_scrolled_window_add_with_viewport ( + GTK_SCROLLED_WINDOW (scrolled_window), tree_view); + gtk_container_set_focus_vadjustment ( + GTK_CONTAINER (tree_view), + gtk_scrolled_window_get_vadjustment ( + GTK_SCROLLED_WINDOW (scrolled_window))); + gtk_container_set_focus_hadjustment ( + GTK_CONTAINER (tree_view), + gtk_scrolled_window_get_hadjustment ( + GTK_SCROLLED_WINDOW (scrolled_window))); + gtk_widget_show (tree_view); + ecde->time_tree_view = tree_view; + g_signal_connect ( + gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)), "changed", + G_CALLBACK (e_cell_date_edit_on_time_selected), ecde); + + bbox = gtk_hbutton_box_new (); + gtk_container_set_border_width (GTK_CONTAINER (bbox), 4); + gtk_box_set_spacing (GTK_BOX (bbox), 2); + gtk_box_pack_start (GTK_BOX (vbox), bbox, FALSE, FALSE, 0); + gtk_widget_show (bbox); + + now_button = gtk_button_new_with_label (_("Now")); + gtk_container_add (GTK_CONTAINER (bbox), now_button); + gtk_widget_show (now_button); + g_signal_connect ( + now_button, "clicked", + G_CALLBACK (e_cell_date_edit_on_now_clicked), ecde); + ecde->now_button = now_button; + + today_button = gtk_button_new_with_label (_("Today")); + gtk_container_add (GTK_CONTAINER (bbox), today_button); + gtk_widget_show (today_button); + g_signal_connect ( + today_button, "clicked", + G_CALLBACK (e_cell_date_edit_on_today_clicked), ecde); + ecde->today_button = today_button; + + /* Translators: "None" as a label of a button to unset date in a + * date table cell. */ + none_button = gtk_button_new_with_label (C_("table-date", "None")); + gtk_container_add (GTK_CONTAINER (bbox), none_button); + gtk_widget_show (none_button); + g_signal_connect ( + none_button, "clicked", + G_CALLBACK (e_cell_date_edit_on_none_clicked), ecde); + ecde->none_button = none_button; + + ok_button = gtk_button_new_with_label (_("OK")); + gtk_container_add (GTK_CONTAINER (bbox), ok_button); + gtk_widget_show (ok_button); + g_signal_connect ( + ok_button, "clicked", + G_CALLBACK (e_cell_date_edit_on_ok_clicked), ecde); + + g_signal_connect ( + ecde->popup_window, "key_press_event", + G_CALLBACK (e_cell_date_edit_key_press), ecde); + g_signal_connect ( + ecde->popup_window, "button_press_event", + G_CALLBACK (e_cell_date_edit_button_press), ecde); +} + +/** + * e_cell_date_edit_new: + * + * Creates a new ECellDateEdit renderer. + * + * Returns: an ECellDateEdit object. + */ +ECell * +e_cell_date_edit_new (void) +{ + return g_object_new (e_cell_date_edit_get_type (), NULL); +} + +static void +e_cell_date_edit_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ECellDateEdit *ecde; + + ecde = E_CELL_DATE_EDIT (object); + + switch (property_id) { + case PROP_SHOW_TIME: + g_value_set_boolean (value, gtk_widget_get_visible (ecde->time_entry)); + return; + case PROP_SHOW_NOW_BUTTON: + g_value_set_boolean (value, gtk_widget_get_visible (ecde->now_button)); + return; + case PROP_SHOW_TODAY_BUTTON: + g_value_set_boolean (value, gtk_widget_get_visible (ecde->today_button)); + return; + case PROP_ALLOW_NO_DATE_SET: + g_value_set_boolean (value, gtk_widget_get_visible (ecde->none_button)); + return; + case PROP_USE_24_HOUR_FORMAT: + g_value_set_boolean (value, ecde->use_24_hour_format); + return; + case PROP_LOWER_HOUR: + g_value_set_int (value, ecde->lower_hour); + return; + case PROP_UPPER_HOUR: + g_value_set_int (value, ecde->upper_hour); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +e_cell_date_edit_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ECellDateEdit *ecde; + gint ivalue; + gboolean bvalue; + + ecde = E_CELL_DATE_EDIT (object); + + switch (property_id) { + case PROP_SHOW_TIME: + if (g_value_get_boolean (value)) { + gtk_widget_show (ecde->time_entry); + gtk_widget_show (ecde->time_tree_view); + } else { + gtk_widget_hide (ecde->time_entry); + gtk_widget_hide (ecde->time_tree_view); + } + return; + case PROP_SHOW_NOW_BUTTON: + if (g_value_get_boolean (value)) { + gtk_widget_show (ecde->now_button); + } else { + gtk_widget_hide (ecde->now_button); + } + return; + case PROP_SHOW_TODAY_BUTTON: + if (g_value_get_boolean (value)) { + gtk_widget_show (ecde->today_button); + } else { + gtk_widget_hide (ecde->today_button); + } + return; + case PROP_ALLOW_NO_DATE_SET: + if (g_value_get_boolean (value)) { + gtk_widget_show (ecde->none_button); + } else { + /* FIXME: What if we have no date set now. */ + gtk_widget_hide (ecde->none_button); + } + return; + case PROP_USE_24_HOUR_FORMAT: + bvalue = g_value_get_boolean (value); + if (ecde->use_24_hour_format != bvalue) { + ecde->use_24_hour_format = bvalue; + ecde->need_time_list_rebuild = TRUE; + } + return; + case PROP_LOWER_HOUR: + ivalue = g_value_get_int (value); + ivalue = CLAMP (ivalue, 0, 24); + if (ecde->lower_hour != ivalue) { + ecde->lower_hour = ivalue; + ecde->need_time_list_rebuild = TRUE; + } + return; + case PROP_UPPER_HOUR: + ivalue = g_value_get_int (value); + ivalue = CLAMP (ivalue, 0, 24); + if (ecde->upper_hour != ivalue) { + ecde->upper_hour = ivalue; + ecde->need_time_list_rebuild = TRUE; + } + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +e_cell_date_edit_dispose (GObject *object) +{ + ECellDateEdit *ecde = E_CELL_DATE_EDIT (object); + + e_cell_date_edit_set_get_time_callback (ecde, NULL, NULL, NULL); + + if (ecde->popup_window != NULL) { + gtk_widget_destroy (ecde->popup_window); + ecde->popup_window = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_cell_date_edit_parent_class)->dispose (object); +} + +static gint +e_cell_date_edit_do_popup (ECellPopup *ecp, + GdkEvent *event, + gint row, + gint view_col) +{ + ECellDateEdit *ecde = E_CELL_DATE_EDIT (ecp); + GdkWindow *window; + + e_cell_date_edit_show_popup (ecde, row, view_col); + e_cell_date_edit_set_popup_values (ecde); + + gtk_grab_add (ecde->popup_window); + + /* Set the focus to the first widget. */ + gtk_widget_grab_focus (ecde->time_entry); + window = gtk_widget_get_window (ecde->popup_window); + gdk_window_focus (window, GDK_CURRENT_TIME); + + return TRUE; +} + +static void +e_cell_date_edit_set_popup_values (ECellDateEdit *ecde) +{ + ECellPopup *ecp = E_CELL_POPUP (ecde); + ECellText *ecell_text = E_CELL_TEXT (ecp->child); + ECellView *ecv = (ECellView *) ecp->popup_cell_view; + ETableItem *eti; + ETableCol *ecol; + gchar *cell_text; + ETimeParseStatus status; + struct tm date_tm; + GDate date; + ECalendarItem *calitem; + gchar buffer[64]; + gboolean is_date = TRUE; + + eti = E_TABLE_ITEM (ecp->popup_cell_view->cell_view.e_table_item_view); + ecol = e_table_header_get_column (eti->header, ecp->popup_view_col); + + cell_text = e_cell_text_get_text ( + ecell_text, ecv->e_table_model, + ecol->col_idx, ecp->popup_row); + + /* Try to parse just a date first. If the value is only a date, we + * use a DATE value. */ + status = e_time_parse_date (cell_text, &date_tm); + if (status == E_TIME_PARSE_INVALID) { + is_date = FALSE; + status = e_time_parse_date_and_time (cell_text, &date_tm); + } + + /* If there is no date and time set, or the date is invalid, we clear + * the selections, else we select the appropriate date & time. */ + calitem = E_CALENDAR_ITEM (E_CALENDAR (ecde->calendar)->calitem); + if (status == E_TIME_PARSE_NONE || status == E_TIME_PARSE_INVALID) { + gtk_entry_set_text (GTK_ENTRY (ecde->time_entry), ""); + e_calendar_item_set_selection (calitem, NULL, NULL); + gtk_tree_selection_unselect_all ( + gtk_tree_view_get_selection ( + GTK_TREE_VIEW (ecde->time_tree_view))); + } else { + if (is_date) { + buffer[0] = '\0'; + } else { + e_time_format_time ( + &date_tm, ecde->use_24_hour_format, + FALSE, buffer, sizeof (buffer)); + } + gtk_entry_set_text (GTK_ENTRY (ecde->time_entry), buffer); + + g_date_clear (&date, 1); + g_date_set_dmy ( + &date, + date_tm.tm_mday, + date_tm.tm_mon + 1, + date_tm.tm_year + 1900); + e_calendar_item_set_selection (calitem, &date, &date); + + if (is_date) { + gtk_tree_selection_unselect_all ( + gtk_tree_view_get_selection ( + GTK_TREE_VIEW (ecde->time_tree_view))); + } else { + e_cell_date_edit_select_matching_time (ecde, buffer); + } + } + + e_cell_text_free_text (ecell_text, cell_text); +} + +static void +e_cell_date_edit_select_matching_time (ECellDateEdit *ecde, + gchar *time) +{ + gboolean found = FALSE; + gboolean valid; + GtkTreeSelection *selection; + GtkTreeIter iter; + GtkTreeModel *model; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (ecde->time_tree_view)); + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ecde->time_tree_view)); + + for (valid = gtk_tree_model_get_iter_first (model, &iter); + valid && !found; + valid = gtk_tree_model_iter_next (model, &iter)) { + gchar *str = NULL; + + gtk_tree_model_get (model, &iter, 0, &str, -1); + + if (g_str_equal (str, time)) { + GtkTreePath *path = gtk_tree_model_get_path (model, &iter); + + gtk_tree_view_set_cursor ( + GTK_TREE_VIEW (ecde->time_tree_view), + path, NULL, FALSE); + gtk_tree_view_scroll_to_cell ( + GTK_TREE_VIEW (ecde->time_tree_view), + path, NULL, FALSE, 0.0, 0.0); + gtk_tree_path_free (path); + + found = TRUE; + } + + g_free (str); + } + + if (!found) { + gtk_tree_selection_unselect_all (selection); + gtk_tree_view_scroll_to_point (GTK_TREE_VIEW (ecde->time_tree_view), 0, 0); + } +} + +static void +e_cell_date_edit_show_popup (ECellDateEdit *ecde, + gint row, + gint view_col) +{ + GdkWindow *window; + gint x, y, width, height; + + if (ecde->need_time_list_rebuild) + e_cell_date_edit_rebuild_time_list (ecde); + + /* This code is practically copied from GtkCombo. */ + + e_cell_date_edit_get_popup_pos (ecde, row, view_col, &x, &y, &height, &width); + + window = gtk_widget_get_window (ecde->popup_window); + gtk_window_move (GTK_WINDOW (ecde->popup_window), x, y); + gtk_widget_set_size_request (ecde->popup_window, width, height); + gtk_widget_realize (ecde->popup_window); + gdk_window_resize (window, width, height); + gtk_widget_show (ecde->popup_window); + + e_cell_popup_set_shown (E_CELL_POPUP (ecde), TRUE); +} + +/* Calculates the size and position of the popup window (like GtkCombo). */ +static void +e_cell_date_edit_get_popup_pos (ECellDateEdit *ecde, + gint row, + gint view_col, + gint *x, + gint *y, + gint *height, + gint *width) +{ + ECellPopup *ecp = E_CELL_POPUP (ecde); + ETableItem *eti; + GtkWidget *canvas; + GtkRequisition popup_requisition; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + GdkWindow *window; + gint avail_height, screen_width, column_width, row_height; + gdouble x1, y1, wx, wy; + gint value; + + eti = E_TABLE_ITEM (ecp->popup_cell_view->cell_view.e_table_item_view); + canvas = GTK_WIDGET (GNOME_CANVAS_ITEM (eti)->canvas); + + window = gtk_widget_get_window (canvas); + gdk_window_get_origin (window, x, y); + + x1 = e_table_header_col_diff (eti->header, 0, view_col + 1); + y1 = e_table_item_row_diff (eti, 0, row + 1); + column_width = e_table_header_col_diff ( + eti->header, view_col, view_col + 1); + row_height = e_table_item_row_diff (eti, row, row + 1); + gnome_canvas_item_i2w (GNOME_CANVAS_ITEM (eti), &x1, &y1); + + gnome_canvas_world_to_window ( + GNOME_CANVAS (canvas), x1, y1, &wx, &wy); + + x1 = wx; + y1 = wy; + + *x += x1; + /* The ETable positions don't include the grid lines, I think, so we + * add 1. */ + scrollable = GTK_SCROLLABLE (&GNOME_CANVAS (canvas)->layout); + adjustment = gtk_scrollable_get_vadjustment (scrollable); + value = (gint) gtk_adjustment_get_value (adjustment); + *y += y1 + 1 - value + ((GnomeCanvas *)canvas)->zoom_yofs; + + avail_height = gdk_screen_height () - *y; + + /* We'll use the entire screen width if needed, but we save space for + * the vertical scrollbar in case we need to show that. */ + screen_width = gdk_screen_width (); + + gtk_widget_get_preferred_size (ecde->popup_window, &popup_requisition, NULL); + + /* Calculate the desired width. */ + *width = popup_requisition.width; + + /* Use at least the same width as the column. */ + if (*width < column_width) + *width = column_width; + + /* Check if it fits in the available height. */ + if (popup_requisition.height > avail_height) { + /* It doesn't fit, so we see if we have the minimum space + * needed. */ + if (*y - row_height > avail_height) { + /* We don't, so we show the popup above the cell + * instead of below it. */ + *y -= (popup_requisition.height + row_height); + if (*y < 0) + *y = 0; + } + } + + /* We try to line it up with the right edge of the column, but we don't + * want it to go off the edges of the screen. */ + if (*x > screen_width) + *x = screen_width; + *x -= *width; + if (*x < 0) + *x = 0; + + *height = popup_requisition.height; +} + +/* This handles key press events in the popup window. If the Escape key is + * pressed we hide the popup, and do not change the cell contents. */ +static gint +e_cell_date_edit_key_press (GtkWidget *popup_window, + GdkEventKey *event, + ECellDateEdit *ecde) +{ + /* If the Escape key is pressed we hide the popup. */ + if (event->keyval != GDK_KEY_Escape) + return FALSE; + + e_cell_date_edit_hide_popup (ecde); + + return TRUE; +} + +/* This handles button press events in the popup window. If the button is + * pressed outside the popup, we hide it and do not change the cell contents. +*/ +static gint +e_cell_date_edit_button_press (GtkWidget *popup_window, + GdkEvent *button_event, + ECellDateEdit *ecde) +{ + GtkWidget *event_widget; + + event_widget = gtk_get_event_widget (button_event); + + if (gtk_widget_get_toplevel (event_widget) != popup_window) + e_cell_date_edit_hide_popup (ecde); + + return TRUE; +} + +/* Clears the time list and rebuilds it using the lower_hour, upper_hour + * and use_24_hour_format settings. */ +static void +e_cell_date_edit_rebuild_time_list (ECellDateEdit *ecde) +{ + GtkListStore *store; + gchar buffer[40]; + struct tm tmp_tm; + gint hour, min; + + store = GTK_LIST_STORE (gtk_tree_view_get_model ( + GTK_TREE_VIEW (ecde->time_tree_view))); + gtk_list_store_clear (store); + + /* Fill the struct tm with some sane values. */ + tmp_tm.tm_year = 2000; + tmp_tm.tm_mon = 0; + tmp_tm.tm_mday = 1; + tmp_tm.tm_sec = 0; + tmp_tm.tm_isdst = 0; + + for (hour = ecde->lower_hour; hour <= ecde->upper_hour; hour++) { + /* We don't want to display midnight at the end, since that is + * really in the next day. */ + if (hour == 24) + break; + + /* We want to finish on upper_hour, with min == 0. */ + for (min = 0; + min == 0 || (min < 60 && hour != ecde->upper_hour); + min += 30) { + GtkTreeIter iter; + + tmp_tm.tm_hour = hour; + tmp_tm.tm_min = min; + e_time_format_time (&tmp_tm, ecde->use_24_hour_format, + FALSE, buffer, sizeof (buffer)); + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, buffer, -1); + } + } + + ecde->need_time_list_rebuild = FALSE; +} + +static void +e_cell_date_edit_on_ok_clicked (GtkWidget *button, + ECellDateEdit *ecde) +{ + ECalendarItem *calitem; + GDate start_date, end_date; + gboolean day_selected; + struct tm date_tm; + gchar buffer[64]; + const gchar *text; + ETimeParseStatus status; + gboolean is_date = FALSE; + + calitem = E_CALENDAR_ITEM (E_CALENDAR (ecde->calendar)->calitem); + day_selected = e_calendar_item_get_selection ( + calitem, &start_date, &end_date); + + text = gtk_entry_get_text (GTK_ENTRY (ecde->time_entry)); + status = e_time_parse_time (text, &date_tm); + if (status == E_TIME_PARSE_INVALID) { + e_cell_date_edit_show_time_invalid_warning (ecde); + return; + } else if (status == E_TIME_PARSE_NONE) { + is_date = TRUE; + } + + if (day_selected) { + date_tm.tm_year = g_date_get_year (&start_date) - 1900; + date_tm.tm_mon = g_date_get_month (&start_date) - 1; + date_tm.tm_mday = g_date_get_day (&start_date); + /* We need to call this to set the weekday. */ + mktime (&date_tm); + e_time_format_date_and_time (&date_tm, + ecde->use_24_hour_format, + !is_date, FALSE, + buffer, sizeof (buffer)); + } else { + buffer[0] = '\0'; + } + + e_cell_date_edit_update_cell (ecde, buffer); + e_cell_date_edit_hide_popup (ecde); +} + +static void +e_cell_date_edit_show_time_invalid_warning (ECellDateEdit *ecde) +{ + GtkWidget *dialog; + struct tm date_tm; + gchar buffer[64]; + + /* Create a useful error message showing the correct format. */ + date_tm.tm_year = 100; + date_tm.tm_mon = 0; + date_tm.tm_mday = 1; + date_tm.tm_hour = 1; + date_tm.tm_min = 30; + date_tm.tm_sec = 0; + date_tm.tm_isdst = -1; + e_time_format_time (&date_tm, ecde->use_24_hour_format, FALSE, + buffer, sizeof (buffer)); + + /* FIXME: Fix transient settings - I'm not sure it works with popup + * windows. Maybe we need to use a normal window without decorations.*/ + dialog = gtk_message_dialog_new ( + GTK_WINDOW (ecde->popup_window), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + _("The time must be in the format: %s"), + buffer); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); +} + +static void +e_cell_date_edit_on_now_clicked (GtkWidget *button, + ECellDateEdit *ecde) +{ + struct tm tmp_tm; + time_t t; + gchar buffer[64]; + + if (ecde->time_callback) { + tmp_tm = ecde->time_callback ( + ecde, ecde->time_callback_data); + } else { + t = time (NULL); + tmp_tm = *localtime (&t); + } + + e_time_format_date_and_time ( + &tmp_tm, ecde->use_24_hour_format, + TRUE, FALSE, buffer, sizeof (buffer)); + + e_cell_date_edit_update_cell (ecde, buffer); + e_cell_date_edit_hide_popup (ecde); +} + +static void +e_cell_date_edit_on_none_clicked (GtkWidget *button, + ECellDateEdit *ecde) +{ + e_cell_date_edit_update_cell (ecde, ""); + e_cell_date_edit_hide_popup (ecde); +} + +static void +e_cell_date_edit_on_today_clicked (GtkWidget *button, + ECellDateEdit *ecde) +{ + struct tm tmp_tm; + time_t t; + gchar buffer[64]; + + if (ecde->time_callback) { + tmp_tm = ecde->time_callback ( + ecde, ecde->time_callback_data); + } else { + t = time (NULL); + tmp_tm = *localtime (&t); + } + + tmp_tm.tm_hour = 0; + tmp_tm.tm_min = 0; + tmp_tm.tm_sec = 0; + + e_time_format_date_and_time ( + &tmp_tm, ecde->use_24_hour_format, + FALSE, FALSE, buffer, sizeof (buffer)); + + e_cell_date_edit_update_cell (ecde, buffer); + e_cell_date_edit_hide_popup (ecde); +} + +static void +e_cell_date_edit_update_cell (ECellDateEdit *ecde, + const gchar *text) +{ + ECellPopup *ecp = E_CELL_POPUP (ecde); + ECellText *ecell_text = E_CELL_TEXT (ecp->child); + ECellView *ecv = (ECellView *) ecp->popup_cell_view; + ETableItem *eti = E_TABLE_ITEM (ecv->e_table_item_view); + ETableCol *ecol; + gchar *old_text; + + /* Compare the new text with the existing cell contents. */ + ecol = e_table_header_get_column (eti->header, ecp->popup_view_col); + + old_text = e_cell_text_get_text ( + ecell_text, ecv->e_table_model, + ecol->col_idx, ecp->popup_row); + + /* If they are different, update the cell contents. */ + if (strcmp (old_text, text)) { + e_cell_text_set_value ( + ecell_text, ecv->e_table_model, + ecol->col_idx, ecp->popup_row, text); + e_cell_leave_edit ( + ecv, ecp->popup_view_col, + ecol->col_idx, ecp->popup_row, NULL); + } + + e_cell_text_free_text (ecell_text, old_text); +} + +static void +e_cell_date_edit_on_time_selected (GtkTreeSelection *selection, + ECellDateEdit *ecde) +{ + gchar *list_item_text = NULL; + GtkTreeIter iter; + GtkTreeModel *model; + + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) + return; + + gtk_tree_model_get (model, &iter, 0, &list_item_text, -1); + + g_return_if_fail (list_item_text != NULL); + + gtk_entry_set_text (GTK_ENTRY (ecde->time_entry), list_item_text); + + g_free (list_item_text); +} + +static void +e_cell_date_edit_hide_popup (ECellDateEdit *ecde) +{ + gtk_grab_remove (ecde->popup_window); + gtk_widget_hide (ecde->popup_window); + e_cell_popup_set_shown (E_CELL_POPUP (ecde), FALSE); +} + +/* These freeze and thaw the rebuilding of the time list. They are useful when + * setting several properties which result in rebuilds of the list, e.g. the + * lower_hour, upper_hour and use_24_hour_format properties. */ +void +e_cell_date_edit_freeze (ECellDateEdit *ecde) +{ + g_return_if_fail (E_IS_CELL_DATE_EDIT (ecde)); + + ecde->freeze_count++; +} + +void +e_cell_date_edit_thaw (ECellDateEdit *ecde) +{ + g_return_if_fail (E_IS_CELL_DATE_EDIT (ecde)); + + if (ecde->freeze_count > 0) { + ecde->freeze_count--; + + if (ecde->freeze_count == 0) + e_cell_date_edit_rebuild_time_list (ecde); + } +} + +/* Sets a callback to use to get the current time. This is useful if the + * application needs to use its own timezone data rather than rely on the + * Unix timezone. */ +void +e_cell_date_edit_set_get_time_callback (ECellDateEdit *ecde, + ECellDateEditGetTimeCallback cb, + gpointer data, + GDestroyNotify destroy) +{ + g_return_if_fail (E_IS_CELL_DATE_EDIT (ecde)); + + if (ecde->time_callback_data && ecde->time_callback_destroy) + (*ecde->time_callback_destroy) (ecde->time_callback_data); + + ecde->time_callback = cb; + ecde->time_callback_data = data; + ecde->time_callback_destroy = destroy; +} diff --git a/e-util/e-cell-date-edit.h b/e-util/e-cell-date-edit.h new file mode 100644 index 0000000000..ea70731457 --- /dev/null +++ b/e-util/e-cell-date-edit.h @@ -0,0 +1,124 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * ECellDateEdit - a subclass of ECellPopup used to show a date with a popup + * window to edit it. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_DATE_EDIT_H_ +#define _E_CELL_DATE_EDIT_H_ + +#include <time.h> + +#include <e-util/e-cell-popup.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_DATE_EDIT \ + (e_cell_date_edit_get_type ()) +#define E_CELL_DATE_EDIT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_DATE_EDIT, ECellDateEdit)) +#define E_CELL_DATE_EDIT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_DATE_EDIT, ECellDateEditClass)) +#define E_IS_CELL_DATE_EDIT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_DATE_EDIT)) +#define E_IS_CELL_DATE_EDIT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_DATE_EDIT)) +#define E_CELL_DATE_EDIT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_DATE_EDIT, ECellDateEditClass)) + +G_BEGIN_DECLS + +typedef struct _ECellDateEdit ECellDateEdit; +typedef struct _ECellDateEditClass ECellDateEditClass; + +/* The type of the callback function optionally used to get the current time. + */ +typedef struct tm (*ECellDateEditGetTimeCallback) (ECellDateEdit *ecde, + gpointer data); + +struct _ECellDateEdit { + ECellPopup parent; + + GtkWidget *popup_window; + GtkWidget *calendar; + GtkWidget *time_entry; + GtkWidget *time_tree_view; + + GtkWidget *now_button; + GtkWidget *today_button; + GtkWidget *none_button; + + /* This is the range of hours we show in the time list. */ + gint lower_hour; + gint upper_hour; + + /* TRUE if we use 24-hour format for the time list and entry. */ + gboolean use_24_hour_format; + + /* This is TRUE if we need to rebuild the list of times. */ + gboolean need_time_list_rebuild; + + /* The freeze count for rebuilding the time list. We only rebuild when + * this is 0. */ + gint freeze_count; + + ECellDateEditGetTimeCallback time_callback; + gpointer time_callback_data; + GDestroyNotify time_callback_destroy; +}; + +struct _ECellDateEditClass { + ECellPopupClass parent_class; +}; + +GType e_cell_date_edit_get_type (void) G_GNUC_CONST; +ECell * e_cell_date_edit_new (void); + +/* These freeze and thaw the rebuilding of the time list. They are useful when + * setting several properties which result in rebuilds of the list, e.g. the + * lower_hour, upper_hour and use_24_hour_format properties. */ +void e_cell_date_edit_freeze (ECellDateEdit *ecde); +void e_cell_date_edit_thaw (ECellDateEdit *ecde); + +/* Sets a callback to use to get the current time. This is useful if the + * application needs to use its own timezone data rather than rely on the + * Unix timezone. */ +void e_cell_date_edit_set_get_time_callback + (ECellDateEdit *ecde, + ECellDateEditGetTimeCallback cb, + gpointer data, + GDestroyNotify destroy); + +G_END_DECLS + +#endif /* _E_CELL_DATE_EDIT_H_ */ diff --git a/e-util/e-cell-date.c b/e-util/e-cell-date.c new file mode 100644 index 0000000000..b8067f208e --- /dev/null +++ b/e-util/e-cell-date.c @@ -0,0 +1,130 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-cell-date.h" + +#include <sys/time.h> +#include <time.h> +#include <unistd.h> +#include <string.h> + +#include <glib/gi18n.h> + +#include "e-datetime-format.h" +#include "e-unicode.h" + +G_DEFINE_TYPE (ECellDate, e_cell_date, E_TYPE_CELL_TEXT) + +static gchar * +ecd_get_text (ECellText *cell, + ETableModel *model, + gint col, + gint row) +{ + time_t date = GPOINTER_TO_INT (e_table_model_value_at (model, col, row)); + const gchar *fmt_component, *fmt_part = NULL; + + if (date == 0) { + return g_strdup (_("?")); + } + + fmt_component = g_object_get_data ((GObject *) cell, "fmt-component"); + if (!fmt_component || !*fmt_component) + fmt_component = "Default"; + else + fmt_part = "table"; + + return e_datetime_format_format ( + fmt_component, fmt_part, DTFormatKindDateTime, date); +} + +static void +ecd_free_text (ECellText *cell, + gchar *text) +{ + g_free (text); +} + +static void +e_cell_date_class_init (ECellDateClass *class) +{ + ECellTextClass *ectc = E_CELL_TEXT_CLASS (class); + + ectc->get_text = ecd_get_text; + ectc->free_text = ecd_free_text; +} + +static void +e_cell_date_init (ECellDate *ecd) +{ +} + +/** + * e_cell_date_new: + * @fontname: font to be used to render on the screen + * @justify: Justification of the string in the cell. + * + * Creates a new ECell renderer that can be used to render dates that + * that come from the model. The value returned from the model is + * interpreted as being a time_t. + * + * The ECellDate object support a large set of properties that can be + * configured through the Gtk argument system and allows the user to have + * a finer control of the way the string is displayed. The arguments supported + * allow the control of strikeout, bold, color and a date filter. + * + * The arguments "strikeout_column", "underline_column", "bold_column" + * and "color_column" set and return an integer that points to a + * column in the model that controls these settings. So controlling + * the way things are rendered is achieved by having special columns + * in the model that will be used to flag whether the date should be + * rendered with strikeout, underline, or bolded. In the case of the + * "color_column" argument, the column in the model is expected to + * have a string that can be parsed by gdk_color_parse(). + * + * Returns: an ECell object that can be used to render dates. + */ +ECell * +e_cell_date_new (const gchar *fontname, + GtkJustification justify) +{ + ECellDate *ecd = g_object_new (E_TYPE_CELL_DATE, NULL); + + e_cell_text_construct (E_CELL_TEXT (ecd), fontname, justify); + + return (ECell *) ecd; +} + +void +e_cell_date_set_format_component (ECellDate *ecd, + const gchar *fmt_component) +{ + g_return_if_fail (ecd != NULL); + + g_object_set_data_full ( + G_OBJECT (ecd), "fmt-component", + g_strdup (fmt_component), g_free); +} diff --git a/e-util/e-cell-date.h b/e-util/e-cell-date.h new file mode 100644 index 0000000000..a0968b050b --- /dev/null +++ b/e-util/e-cell-date.h @@ -0,0 +1,74 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CELL_DATE_H +#define E_CELL_DATE_H + +#include <e-util/e-cell-text.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_DATE \ + (e_cell_date_get_type ()) +#define E_CELL_DATE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_DATE, ECellDate)) +#define E_CELL_DATE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_DATE, ECellDateClass)) +#define E_IS_CELL_DATE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_DATE)) +#define E_IS_CELL_DATE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_DATE)) +#define E_CELL_DATE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_DATE, ECellDateClass)) + +G_BEGIN_DECLS + +typedef struct _ECellDate ECellDate; +typedef struct _ECellDateClass ECellDateClass; + +struct _ECellDate { + ECellText parent; +}; + +struct _ECellDateClass { + ECellTextClass parent_class; +}; + +GType e_cell_date_get_type (void) G_GNUC_CONST; +ECell * e_cell_date_new (const gchar *fontname, + GtkJustification justify); +void e_cell_date_set_format_component + (ECellDate *ecd, + const gchar *fmt_component); + +G_END_DECLS + +#endif /* E_CELL_DATE_H */ diff --git a/e-util/e-cell-hbox.c b/e-util/e-cell-hbox.c new file mode 100644 index 0000000000..669dd4416c --- /dev/null +++ b/e-util/e-cell-hbox.c @@ -0,0 +1,353 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Srinivasa Ragavan <sragavan@novell.com> + * + * A majority of code taken from: + * + * the ECellText renderer. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <ctype.h> +#include <math.h> +#include <stdio.h> + +#include <gtk/gtk.h> + +/* #include "a11y/gal-a11y-e-cell-registry.h" */ +/* #include "a11y/gal-a11y-e-cell-vbox.h" */ + +#include "e-cell-hbox.h" +#include "e-table-item.h" + +G_DEFINE_TYPE (ECellHbox, e_cell_hbox, E_TYPE_CELL) + +#define INDENT_AMOUNT 16 +#define MAX_CELL_SIZE 25 + +/* + * ECell::new_view method + */ +static ECellView * +ecv_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view) +{ + ECellHbox *ecv = E_CELL_HBOX (ecell); + ECellHboxView *hbox_view = g_new0 (ECellHboxView, 1); + gint i; + + hbox_view->cell_view.ecell = ecell; + hbox_view->cell_view.e_table_model = table_model; + hbox_view->cell_view.e_table_item_view = e_table_item_view; + hbox_view->cell_view.kill_view_cb = NULL; + hbox_view->cell_view.kill_view_cb_data = NULL; + + /* create our subcell view */ + hbox_view->subcell_view_count = ecv->subcell_count; + hbox_view->subcell_views = g_new (ECellView *, hbox_view->subcell_view_count); + hbox_view->model_cols = g_new (int, hbox_view->subcell_view_count); + hbox_view->def_size_cols = g_new (int, hbox_view->subcell_view_count); + + for (i = 0; i < hbox_view->subcell_view_count; i++) { + hbox_view->subcell_views[i] = e_cell_new_view (ecv->subcells[i], table_model, e_table_item_view /* XXX */); + hbox_view->model_cols[i] = ecv->model_cols[i]; + hbox_view->def_size_cols[i] = ecv->def_size_cols[i]; + } + + return (ECellView *) hbox_view; +} + +/* + * ECell::kill_view method + */ +static void +ecv_kill_view (ECellView *ecv) +{ + ECellHboxView *hbox_view = (ECellHboxView *) ecv; + gint i; + + if (hbox_view->cell_view.kill_view_cb) + (hbox_view->cell_view.kill_view_cb)(ecv, hbox_view->cell_view.kill_view_cb_data); + + if (hbox_view->cell_view.kill_view_cb_data) + g_list_free (hbox_view->cell_view.kill_view_cb_data); + + /* kill our subcell view */ + for (i = 0; i < hbox_view->subcell_view_count; i++) + e_cell_kill_view (hbox_view->subcell_views[i]); + + g_free (hbox_view->model_cols); + g_free (hbox_view->def_size_cols); + g_free (hbox_view->subcell_views); + g_free (hbox_view); +} + +/* + * ECell::realize method + */ +static void +ecv_realize (ECellView *ecell_view) +{ + ECellHboxView *hbox_view = (ECellHboxView *) ecell_view; + gint i; + + /* realize our subcell view */ + for (i = 0; i < hbox_view->subcell_view_count; i++) + e_cell_realize (hbox_view->subcell_views[i]); + + if (E_CELL_CLASS (e_cell_hbox_parent_class)->realize) + (* E_CELL_CLASS (e_cell_hbox_parent_class)->realize) (ecell_view); +} + +/* + * ECell::unrealize method + */ +static void +ecv_unrealize (ECellView *ecv) +{ + ECellHboxView *hbox_view = (ECellHboxView *) ecv; + gint i; + + /* unrealize our subcell view. */ + for (i = 0; i < hbox_view->subcell_view_count; i++) + e_cell_unrealize (hbox_view->subcell_views[i]); + + if (E_CELL_CLASS (e_cell_hbox_parent_class)->unrealize) + (* E_CELL_CLASS (e_cell_hbox_parent_class)->unrealize) (ecv); +} + +/* + * ECell::draw method + */ +static void +ecv_draw (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2) +{ + ECellHboxView *hbox_view = (ECellHboxView *) ecell_view; + + gint subcell_offset = 0; + gint i; + gint allotted_width = x2 - x1; + + for (i = 0; i < hbox_view->subcell_view_count; i++) { + /* Now cause our subcells to draw their contents, + * shifted by subcell_offset pixels */ + gint width = allotted_width * hbox_view->def_size_cols[i] / 100; + /* e_cell_max_width_by_row (hbox_view->subcell_views[i], hbox_view->model_cols[i], view_col, row); + if (width < hbox_view->def_size_cols[i]) + width = hbox_view->def_size_cols[i]; + printf ("width of %d %d of %d\n", width,hbox_view->def_size_cols[i], allotted_width); */ + + e_cell_draw ( + hbox_view->subcell_views[i], cr, + hbox_view->model_cols[i], view_col, row, flags, + x1 + subcell_offset , y1, + x1 + subcell_offset + width, y2); + + subcell_offset += width; /* e_cell_max_width_by_row (hbox_view->subcell_views[i], hbox_view->model_cols[i], view_col, row); */ + } +} + +/* + * ECell::event method + */ +static gint +ecv_event (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions) +{ + ECellHboxView *hbox_view = (ECellHboxView *) ecell_view; + gint y = 0; + gint i; + gint subcell_offset = 0; + + switch (event->type) { + case GDK_BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + y = event->button.y; + break; + case GDK_MOTION_NOTIFY: + y = event->motion.y; + break; + default: + /* nada */ + break; + } + + for (i = 0; i < hbox_view->subcell_view_count; i++) { + gint width = e_cell_max_width_by_row (hbox_view->subcell_views[i], hbox_view->model_cols[i], view_col, row); + if (width < hbox_view->def_size_cols[i]) + width = hbox_view->def_size_cols[i]; + if (y < subcell_offset + width) + return e_cell_event (hbox_view->subcell_views[i], event, hbox_view->model_cols[i], view_col, row, flags, actions); + subcell_offset += width; + } + return 0; +} + +/* + * ECell::height method + */ +static gint +ecv_height (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + ECellHboxView *hbox_view = (ECellHboxView *) ecell_view; + gint height = 0, max_height = 0; + gint i; + + for (i = 0; i < hbox_view->subcell_view_count; i++) { + height = e_cell_height (hbox_view->subcell_views[i], hbox_view->model_cols[i], view_col, row); + max_height = MAX (max_height, height); + } + return max_height; +} + +/* + * ECell::max_width method + */ +static gint +ecv_max_width (ECellView *ecell_view, + gint model_col, + gint view_col) +{ + ECellHboxView *hbox_view = (ECellHboxView *) ecell_view; + gint width = 0; + gint i; + + for (i = 0; i < hbox_view->subcell_view_count; i++) { + gint cell_width = e_cell_max_width (hbox_view->subcell_views[i], hbox_view->model_cols[i], view_col); + + if (cell_width < hbox_view->def_size_cols[i]) + cell_width = hbox_view->def_size_cols[i]; + width += cell_width; + } + + return width; +} + +/* + * GObject::dispose method + */ +static void +ecv_dispose (GObject *object) +{ + ECellHbox *ecv = E_CELL_HBOX (object); + gint i; + + /* destroy our subcell */ + for (i = 0; i < ecv->subcell_count; i++) + if (ecv->subcells[i]) + g_object_unref (ecv->subcells[i]); + g_free (ecv->subcells); + ecv->subcells = NULL; + ecv->subcell_count = 0; + + g_free (ecv->model_cols); + ecv->model_cols = NULL; + + g_free (ecv->def_size_cols); + ecv->def_size_cols = NULL; + + G_OBJECT_CLASS (e_cell_hbox_parent_class)->dispose (object); +} + +static void +e_cell_hbox_class_init (ECellHboxClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + ECellClass *ecc = E_CELL_CLASS (class); + + object_class->dispose = ecv_dispose; + + ecc->new_view = ecv_new_view; + ecc->kill_view = ecv_kill_view; + ecc->realize = ecv_realize; + ecc->unrealize = ecv_unrealize; + ecc->draw = ecv_draw; + ecc->event = ecv_event; + ecc->height = ecv_height; + + ecc->max_width = ecv_max_width; + +/* gal_a11y_e_cell_registry_add_cell_type (NULL, E_TYPE_CELL_HBOX, gal_a11y_e_cell_hbox_new); */ +} + +static void +e_cell_hbox_init (ECellHbox *ecv) +{ + ecv->subcells = NULL; + ecv->subcell_count = 0; +} + +/** + * e_cell_hbox_new: + * + * Creates a new ECell renderer that can be used to render multiple + * child cells. + * + * Return value: an ECell object that can be used to render multiple + * child cells. + **/ +ECell * +e_cell_hbox_new (void) +{ + return g_object_new (E_TYPE_CELL_HBOX, NULL); +} + +void +e_cell_hbox_append (ECellHbox *hbox, + ECell *subcell, + gint model_col, + gint size) +{ + hbox->subcell_count++; + + hbox->subcells = g_renew (ECell *, hbox->subcells, hbox->subcell_count); + hbox->model_cols = g_renew (int, hbox->model_cols, hbox->subcell_count); + hbox->def_size_cols = g_renew (int, hbox->def_size_cols, hbox->subcell_count); + + hbox->subcells[hbox->subcell_count - 1] = subcell; + hbox->model_cols[hbox->subcell_count - 1] = model_col; + hbox->def_size_cols[hbox->subcell_count - 1] = size; + + if (subcell) + g_object_ref_sink (subcell); +} diff --git a/e-util/e-cell-hbox.h b/e-util/e-cell-hbox.h new file mode 100644 index 0000000000..6a0495cf50 --- /dev/null +++ b/e-util/e-cell-hbox.h @@ -0,0 +1,91 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Srinivasa Ragavan <sragavan@novell.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_HBOX_H_ +#define _E_CELL_HBOX_H_ + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-cell.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_HBOX \ + (e_cell_hbox_get_type ()) +#define E_CELL_HBOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_HBOX, ECellHbox)) +#define E_CELL_HBOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_HBOX, ECellHboxClass)) +#define E_IS_CELL_HBOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_HBOX)) +#define E_IS_CELL_HBOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_HBOX)) +#define E_CELL_HBOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_HBOX, ECellHboxClass)) + +G_BEGIN_DECLS + +typedef struct _ECellHbox ECellHbox; +typedef struct _ECellHboxView ECellHboxView; +typedef struct _ECellHboxClass ECellHboxClass; + +struct _ECellHbox { + ECell parent; + + gint subcell_count; + ECell **subcells; + gint *model_cols; + gint *def_size_cols; +}; + +struct _ECellHboxView { + ECellView cell_view; + + gint subcell_view_count; + ECellView **subcell_views; + gint *model_cols; + gint *def_size_cols; +}; + +struct _ECellHboxClass { + ECellClass parent_class; +}; + +GType e_cell_hbox_get_type (void) G_GNUC_CONST; +ECell * e_cell_hbox_new (void); +void e_cell_hbox_append (ECellHbox *vbox, + ECell *subcell, + gint model_col, + gint size); + +G_END_DECLS + +#endif /* _E_CELL_HBOX_H_ */ diff --git a/e-util/e-cell-number.c b/e-util/e-cell-number.c new file mode 100644 index 0000000000..5ecccaded1 --- /dev/null +++ b/e-util/e-cell-number.c @@ -0,0 +1,95 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-cell-number.h" + +#include <sys/time.h> +#include <unistd.h> + +#include <glib/gi18n.h> + +#include "e-misc-utils.h" + +G_DEFINE_TYPE (ECellNumber, e_cell_number, E_TYPE_CELL_TEXT) + +static gchar * +ecn_get_text (ECellText *cell, + ETableModel *model, + gint col, + gint row) +{ + gpointer value; + + value = e_table_model_value_at (model, col, row); + + return e_format_number (GPOINTER_TO_INT (value)); +} + +static void +ecn_free_text (ECellText *cell, + gchar *text) +{ + g_free (text); +} + +static void +e_cell_number_class_init (ECellNumberClass *class) +{ + ECellTextClass *ectc = E_CELL_TEXT_CLASS (class); + + ectc->get_text = ecn_get_text; + ectc->free_text = ecn_free_text; +} + +static void +e_cell_number_init (ECellNumber *cell_number) +{ +} + +/** + * e_cell_number_new: + * @fontname: font to be used to render on the screen + * @justify: Justification of the string in the cell. + * + * Creates a new ECell renderer that can be used to render numbers that + * that come from the model. The value returned from the model is + * interpreted as being an int. + * + * See ECellText for other features. + * + * Returns: an ECell object that can be used to render numbers. + */ +ECell * +e_cell_number_new (const gchar *fontname, + GtkJustification justify) +{ + ECellNumber *ecn = g_object_new (E_TYPE_CELL_NUMBER, NULL); + + e_cell_text_construct (E_CELL_TEXT (ecn), fontname, justify); + + return (ECell *) ecn; +} + diff --git a/e-util/e-cell-number.h b/e-util/e-cell-number.h new file mode 100644 index 0000000000..a5ef2ab52f --- /dev/null +++ b/e-util/e-cell-number.h @@ -0,0 +1,71 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CELL_NUMBER_H +#define E_CELL_NUMBER_H + +#include <e-util/e-cell-text.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_NUMBER \ + (e_cell_number_get_type ()) +#define E_CELL_NUMBER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_NUMBER, ECellNumber)) +#define E_CELL_NUMBER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_NUMBER, ECellNumberClass)) +#define E_IS_CELL_NUMBER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_NUMBER)) +#define E_IS_CELL_NUMBER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_NUMBER)) +#define E_CELL_NUMBER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_NUMBER, ECellNumberClass)) + +G_BEGIN_DECLS + +typedef struct _ECellNumber ECellNumber; +typedef struct _ECellNumberClass ECellNumberClass; + +struct _ECellNumber { + ECellText parent; +}; + +struct _ECellNumberClass { + ECellTextClass parent_class; +}; + +GType e_cell_number_get_type (void) G_GNUC_CONST; +ECell * e_cell_number_new (const gchar *fontname, + GtkJustification justify); + +G_END_DECLS + +#endif /* E_CELL_NUMBER_H */ diff --git a/e-util/e-cell-percent.c b/e-util/e-cell-percent.c new file mode 100644 index 0000000000..81465d5a62 --- /dev/null +++ b/e-util/e-cell-percent.c @@ -0,0 +1,160 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * ECellPercent - a subclass of ECellText used to show an integer percentage + * in an ETable. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <ctype.h> + +#include <sys/time.h> +#include <unistd.h> +#include <stdio.h> +#include <glib/gi18n.h> + +#include "e-cell-percent.h" + +G_DEFINE_TYPE (ECellPercent, e_cell_percent, E_TYPE_CELL_TEXT) + +static gchar * +ecp_get_text (ECellText *cell, + ETableModel *model, + gint col, + gint row) +{ + gint percent; + static gchar buffer[8]; + + percent = GPOINTER_TO_INT (e_table_model_value_at (model, col, row)); + + /* A -ve value means the property is not set. */ + if (percent < 0) { + buffer[0] = '\0'; + } else { + g_snprintf (buffer, sizeof (buffer), "%i%%", percent); + } + + return buffer; +} + +static void +ecp_free_text (ECellText *cell, + gchar *text) +{ + /* Do Nothing. */ +} + +/* FIXME: We need to set the "transient_for" property for the dialog. */ +static void +show_percent_warning (void) +{ + GtkWidget *dialog; + + dialog = gtk_message_dialog_new ( + NULL, 0, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "%s", _("The percent value must be between 0 and 100, inclusive")); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); +} + +static void +ecp_set_value (ECellText *cell, + ETableModel *model, + gint col, + gint row, + const gchar *text) +{ + gint matched, percent; + gboolean empty = TRUE; + const gchar *p; + + if (text) { + p = text; + while (*p) { + if (!isspace ((guchar) *p)) { + empty = FALSE; + break; + } + p++; + } + } + + if (empty) { + percent = -1; + } else { + matched = sscanf (text, "%i", &percent); + + if (matched != 1 || percent < 0 || percent > 100) { + show_percent_warning (); + return; + } + } + + e_table_model_set_value_at ( + model, col, row, + GINT_TO_POINTER (percent)); +} + +static void +e_cell_percent_class_init (ECellPercentClass *ecpc) +{ + ECellTextClass *ectc = (ECellTextClass *) ecpc; + + ectc->get_text = ecp_get_text; + ectc->free_text = ecp_free_text; + ectc->set_value = ecp_set_value; +} + +static void +e_cell_percent_init (ECellPercent *ecp) +{ +} + +/** + * e_cell_percent_new: + * @fontname: font to be used to render on the screen + * @justify: Justification of the string in the cell. + * + * Creates a new ECell renderer that can be used to render an integer + * percentage that comes from the model. The value returned from the model is + * interpreted as being an int. + * + * See ECellText for other features. + * + * Returns: an ECell object that can be used to render numbers. + */ +ECell * +e_cell_percent_new (const gchar *fontname, + GtkJustification justify) +{ + ECellPercent *ecn = g_object_new (E_TYPE_CELL_PERCENT, NULL); + + e_cell_text_construct (E_CELL_TEXT (ecn), fontname, justify); + + return (ECell *) ecn; +} diff --git a/e-util/e-cell-percent.h b/e-util/e-cell-percent.h new file mode 100644 index 0000000000..57ce41d1a3 --- /dev/null +++ b/e-util/e-cell-percent.h @@ -0,0 +1,76 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * ECellPercent - a subclass of ECellText used to show an integer percentage + * in an ETable. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CELL_PERCENT_H +#define E_CELL_PERCENT_H + +#include <e-util/e-cell-text.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_PERCENT \ + (e_cell_percent_get_type ()) +#define E_CELL_PERCENT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_PERCENT, ECellPercent)) +#define E_CELL_PERCENT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_PERCENT, ECellPercentClass)) +#define E_IS_CELL_PERCENT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_PERCENT)) +#define E_IS_CELL_PERCENT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_PERCENT)) +#define E_CELL_PERCENT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_PERCENT, ECellPercentClass)) + +G_BEGIN_DECLS + +typedef struct _ECellPercent ECellPercent; +typedef struct _ECellPercentClass ECellPercentClass; + +struct _ECellPercent { + ECellText parent; +}; + +struct _ECellPercentClass { + ECellTextClass parent_class; +}; + +GType e_cell_percent_get_type (void) G_GNUC_CONST; +ECell * e_cell_percent_new (const gchar *fontname, + GtkJustification justify); + +G_END_DECLS + +#endif /* E_CELL_PERCENT_H */ diff --git a/e-util/e-cell-pixbuf.c b/e-util/e-cell-pixbuf.c new file mode 100644 index 0000000000..41b030ec5a --- /dev/null +++ b/e-util/e-cell-pixbuf.c @@ -0,0 +1,389 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Vladimir Vukicevic <vladimir@ximian.com> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include "e-cell-pixbuf.h" + +G_DEFINE_TYPE (ECellPixbuf, e_cell_pixbuf, E_TYPE_CELL) + +typedef struct _ECellPixbufView ECellPixbufView; + +struct _ECellPixbufView { + ECellView cell_view; + GnomeCanvas *canvas; +}; + +/* Object argument IDs */ +enum { + PROP_0, + + PROP_SELECTED_COLUMN, + PROP_FOCUSED_COLUMN, + PROP_UNSELECTED_COLUMN +}; + +/* + * ECellPixbuf functions + */ + +ECell * +e_cell_pixbuf_new (void) +{ + return g_object_new (E_TYPE_CELL_PIXBUF, NULL); +} + +/* + * ECell methods + */ + +static ECellView * +pixbuf_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view) +{ + ECellPixbufView *pixbuf_view = g_new0 (ECellPixbufView, 1); + ETableItem *eti = E_TABLE_ITEM (e_table_item_view); + GnomeCanvas *canvas = GNOME_CANVAS_ITEM (eti)->canvas; + + pixbuf_view->cell_view.ecell = ecell; + pixbuf_view->cell_view.e_table_model = table_model; + pixbuf_view->cell_view.e_table_item_view = e_table_item_view; + pixbuf_view->cell_view.kill_view_cb = NULL; + pixbuf_view->cell_view.kill_view_cb_data = NULL; + + pixbuf_view->canvas = canvas; + + return (ECellView *) pixbuf_view; +} + +static void +pixbuf_kill_view (ECellView *ecell_view) +{ + ECellPixbufView *pixbuf_view = (ECellPixbufView *) ecell_view; + + if (pixbuf_view->cell_view.kill_view_cb) + pixbuf_view->cell_view.kill_view_cb ( + ecell_view, pixbuf_view->cell_view.kill_view_cb_data); + + if (pixbuf_view->cell_view.kill_view_cb_data) + g_list_free (pixbuf_view->cell_view.kill_view_cb_data); + + g_free (pixbuf_view); +} + +static void +pixbuf_draw (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2) +{ + GdkPixbuf *cell_pixbuf; + gint real_x, real_y; + gint pix_w, pix_h; + + cell_pixbuf = e_table_model_value_at (ecell_view->e_table_model, + 1, row); + + /* we can't make sure we really got a pixbuf since, well, it's a Gdk thing */ + + if (x2 - x1 == 0) + return; + + if (!cell_pixbuf) + return; + + pix_w = gdk_pixbuf_get_width (cell_pixbuf); + pix_h = gdk_pixbuf_get_height (cell_pixbuf); + + /* We center the pixbuf within our allocated space */ + if (x2 - x1 > pix_w) { + gint diff = (x2 - x1) - pix_w; + real_x = x1 + diff / 2; + } else { + real_x = x1; + } + + if (y2 - y1 > pix_h) { + gint diff = (y2 - y1) - pix_h; + real_y = y1 + diff / 2; + } else { + real_y = y1; + } + + cairo_save (cr); + gdk_cairo_set_source_pixbuf (cr, cell_pixbuf, real_x, real_y); + cairo_paint_with_alpha (cr, 1); + cairo_restore (cr); +} + +static gint +pixbuf_event (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions) +{ + /* noop */ + + return FALSE; +} + +static gint +pixbuf_height (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + GdkPixbuf *pixbuf; + if (row == -1) { + if (e_table_model_row_count (ecell_view->e_table_model) > 0) { + row = 0; + } else { + return 6; + } + } + + pixbuf = (GdkPixbuf *) e_table_model_value_at (ecell_view->e_table_model, 1, row); + if (!pixbuf) + return 0; + + /* We give ourselves 3 pixels of padding on either side */ + return gdk_pixbuf_get_height (pixbuf) + 6; +} + +/* + * ECell::print method + */ +static void +pixbuf_print (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height) +{ + GdkPixbuf *pixbuf; + gint scale; + cairo_t *cr = gtk_print_context_get_cairo_context (context); + + pixbuf = (GdkPixbuf *) e_table_model_value_at (ecell_view->e_table_model, 1, row); + if (pixbuf == NULL) + return; + + scale = gdk_pixbuf_get_height (pixbuf); + cairo_save (cr); + cairo_translate (cr, 0, (gdouble)(height - scale) / (gdouble) 2); + gdk_cairo_set_source_pixbuf (cr, pixbuf, (gdouble) scale, (gdouble) scale); + cairo_paint (cr); + cairo_restore (cr); +} + +static gdouble +pixbuf_print_height (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width) +{ + GdkPixbuf *pixbuf; + + if (row == -1) { + if (e_table_model_row_count (ecell_view->e_table_model) > 0) { + row = 0; + } else { + return 6; + } + } + + pixbuf = (GdkPixbuf *) e_table_model_value_at (ecell_view->e_table_model, 1, row); + if (!pixbuf) + return 0; + + /* We give ourselves 3 pixels of padding on either side */ + return gdk_pixbuf_get_height (pixbuf); +} + +static gint +pixbuf_max_width (ECellView *ecell_view, + gint model_col, + gint view_col) +{ + gint pw; + gint num_rows, i; + gint max_width = -1; + + if (model_col == 0) { + num_rows = e_table_model_row_count (ecell_view->e_table_model); + + for (i = 0; i <= num_rows; i++) { + GdkPixbuf *pixbuf = (GdkPixbuf *) e_table_model_value_at + (ecell_view->e_table_model, + 1, + i); + if (!pixbuf) + continue; + pw = gdk_pixbuf_get_width (pixbuf); + if (max_width < pw) + max_width = pw; + } + } else { + return -1; + } + + return max_width; +} + +static void +pixbuf_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ECellPixbuf *pixbuf; + + pixbuf = E_CELL_PIXBUF (object); + + switch (property_id) { + case PROP_SELECTED_COLUMN: + pixbuf->selected_column = g_value_get_int (value); + break; + + case PROP_FOCUSED_COLUMN: + pixbuf->focused_column = g_value_get_int (value); + break; + + case PROP_UNSELECTED_COLUMN: + pixbuf->unselected_column = g_value_get_int (value); + break; + + default: + return; + } +} + +/* Get_arg handler for the pixbuf item */ +static void +pixbuf_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ECellPixbuf *pixbuf; + + pixbuf = E_CELL_PIXBUF (object); + + switch (property_id) { + case PROP_SELECTED_COLUMN: + g_value_set_int (value, pixbuf->selected_column); + break; + + case PROP_FOCUSED_COLUMN: + g_value_set_int (value, pixbuf->focused_column); + break; + + case PROP_UNSELECTED_COLUMN: + g_value_set_int (value, pixbuf->unselected_column); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +e_cell_pixbuf_init (ECellPixbuf *ecp) +{ + ecp->selected_column = -1; + ecp->focused_column = -1; + ecp->unselected_column = -1; +} + +static void +e_cell_pixbuf_class_init (ECellPixbufClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + ECellClass *ecc = E_CELL_CLASS (class); + + object_class->set_property = pixbuf_set_property; + object_class->get_property = pixbuf_get_property; + + ecc->new_view = pixbuf_new_view; + ecc->kill_view = pixbuf_kill_view; + ecc->draw = pixbuf_draw; + ecc->event = pixbuf_event; + ecc->height = pixbuf_height; + ecc->print = pixbuf_print; + ecc->print_height = pixbuf_print_height; + ecc->max_width = pixbuf_max_width; + + g_object_class_install_property ( + object_class, + PROP_SELECTED_COLUMN, + g_param_spec_int ( + "selected_column", + "Selected Column", + NULL, + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_FOCUSED_COLUMN, + g_param_spec_int ( + "focused_column", + "Focused Column", + NULL, + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_UNSELECTED_COLUMN, + g_param_spec_int ( + "unselected_column", + "Unselected Column", + NULL, + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); +} + diff --git a/e-util/e-cell-pixbuf.h b/e-util/e-cell-pixbuf.h new file mode 100644 index 0000000000..e76fcab2c7 --- /dev/null +++ b/e-util/e-cell-pixbuf.h @@ -0,0 +1,75 @@ +/* + * e-cell-pixbuf.h - An ECell that displays a GdkPixbuf + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Vladimir Vukicevic <vladimir@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_PIXBUF_H_ +#define _E_CELL_PIXBUF_H_ + +#include <e-util/e-table.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_PIXBUF \ + (e_cell_pixbuf_get_type ()) +#define E_CELL_PIXBUF(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_PIXBUF, ECellPixbuf)) +#define E_CELL_PIXBUF_CLASS(cls) \ + (G_TYPE_CHECK_INSTANCE_CAST_CLASS \ + ((cls), E_TYPE_CELL_PIXBUF, ECellPixbufClass)) +#define E_IS_CELL_PIXBUF(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_PIXBUF)) +#define E_IS_CELL_PIXBUF_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_PIXBUF)) +#define E_CELL_PIXBUF_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_PIXBUF, ECellPixbufClass)) + +G_BEGIN_DECLS + +typedef struct _ECellPixbuf ECellPixbuf; +typedef struct _ECellPixbufClass ECellPixbufClass; + +struct _ECellPixbuf { + ECell parent; + + gint selected_column; + gint focused_column; + gint unselected_column; +}; + +struct _ECellPixbufClass { + ECellClass parent_class; +}; + +GType e_cell_pixbuf_get_type (void) G_GNUC_CONST; +ECell * e_cell_pixbuf_new (void); + +G_END_DECLS + +#endif /* _E_CELL_PIXBUF_H */ diff --git a/e-util/e-cell-popup.c b/e-util/e-cell-popup.c new file mode 100644 index 0000000000..19c32a658d --- /dev/null +++ b/e-util/e-cell-popup.c @@ -0,0 +1,550 @@ +/* + * e-cell-popup.c: Popup cell renderer + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * ECellPopup - an abstract ECell class used to support popup selections like + * a GtkCombo widget. It contains a child ECell, e.g. an ECellText, but when + * selected it displays an arrow on the right edge which the user can click to + * show a popup. Subclasses implement the popup class function to show the + * popup. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gdk/gdkkeysyms.h> + +#include "gal-a11y-e-cell-popup.h" +#include "gal-a11y-e-cell-registry.h" + +#include "e-cell-popup.h" +#include "e-table-item.h" +#include <gtk/gtk.h> + +#define E_CELL_POPUP_ARROW_SIZE 16 +#define E_CELL_POPUP_ARROW_PAD 3 + +static void e_cell_popup_dispose (GObject *object); + +static ECellView * ecp_new_view (ECell *ecell, + ETableModel *table_model, + void *e_table_item_view); +static void ecp_kill_view (ECellView *ecv); +static void ecp_realize (ECellView *ecv); +static void ecp_unrealize (ECellView *ecv); +static void ecp_draw (ECellView *ecv, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2); +static gint ecp_event (ECellView *ecv, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions); +static gint ecp_height (ECellView *ecv, + gint model_col, + gint view_col, + gint row); +static gpointer ecp_enter_edit (ECellView *ecv, + gint model_col, + gint view_col, + gint row); +static void ecp_leave_edit (ECellView *ecv, + gint model_col, + gint view_col, + gint row, + void *edit_context); +static void ecp_print (ECellView *ecv, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height); +static gdouble ecp_print_height (ECellView *ecv, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width); +static gint ecp_max_width (ECellView *ecv, + gint model_col, + gint view_col); +static gchar *ecp_get_bg_color (ECellView *ecell_view, gint row); + +static gint e_cell_popup_do_popup (ECellPopupView *ecp_view, + GdkEvent *event, + gint row, + gint model_col); + +G_DEFINE_TYPE (ECellPopup, e_cell_popup, E_TYPE_CELL) + +static void +e_cell_popup_class_init (ECellPopupClass *class) +{ + ECellClass *ecc = E_CELL_CLASS (class); + + G_OBJECT_CLASS (class)->dispose = e_cell_popup_dispose; + + ecc->new_view = ecp_new_view; + ecc->kill_view = ecp_kill_view; + ecc->realize = ecp_realize; + ecc->unrealize = ecp_unrealize; + ecc->draw = ecp_draw; + ecc->event = ecp_event; + ecc->height = ecp_height; + ecc->enter_edit = ecp_enter_edit; + ecc->leave_edit = ecp_leave_edit; + ecc->print = ecp_print; + ecc->print_height = ecp_print_height; + ecc->max_width = ecp_max_width; + ecc->get_bg_color = ecp_get_bg_color; + + gal_a11y_e_cell_registry_add_cell_type ( + NULL, E_TYPE_CELL_POPUP, + gal_a11y_e_cell_popup_new); +} + +static void +e_cell_popup_init (ECellPopup *ecp) +{ + ecp->popup_shown = FALSE; + ecp->popup_model = NULL; +} + +/** + * e_cell_popup_new: + * + * Creates a new ECellPopup renderer. + * + * Returns: an ECellPopup object. + */ +ECell * +e_cell_popup_new (void) +{ + return g_object_new (E_TYPE_CELL_POPUP, NULL); +} + +static void +e_cell_popup_dispose (GObject *object) +{ + ECellPopup *ecp = E_CELL_POPUP (object); + + if (ecp->child) + g_object_unref (ecp->child); + ecp->child = NULL; + + G_OBJECT_CLASS (e_cell_popup_parent_class)->dispose (object); +} + +/* + * ECell::new_view method + */ +static ECellView * +ecp_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view) +{ + ECellPopup *ecp = E_CELL_POPUP (ecell); + ECellPopupView *ecp_view; + + /* We must have a child ECell before we create any views. */ + g_return_val_if_fail (ecp->child != NULL, NULL); + + ecp_view = g_new0 (ECellPopupView, 1); + + ecp_view->cell_view.ecell = ecell; + ecp_view->cell_view.e_table_model = table_model; + ecp_view->cell_view.e_table_item_view = e_table_item_view; + ecp_view->cell_view.kill_view_cb = NULL; + ecp_view->cell_view.kill_view_cb_data = NULL; + + ecp_view->child_view = e_cell_new_view ( + ecp->child, table_model, + e_table_item_view); + + return (ECellView *) ecp_view; +} + +/* + * ECell::kill_view method + */ +static void +ecp_kill_view (ECellView *ecv) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + + if (ecp_view->cell_view.kill_view_cb) + ecp_view->cell_view.kill_view_cb ( + ecv, ecp_view->cell_view.kill_view_cb_data); + + if (ecp_view->cell_view.kill_view_cb_data) + g_list_free (ecp_view->cell_view.kill_view_cb_data); + + if (ecp_view->child_view) + e_cell_kill_view (ecp_view->child_view); + + g_free (ecp_view); +} + +/* + * ECell::realize method + */ +static void +ecp_realize (ECellView *ecv) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + + e_cell_realize (ecp_view->child_view); + + if (E_CELL_CLASS (e_cell_popup_parent_class)->realize) + (* E_CELL_CLASS (e_cell_popup_parent_class)->realize) (ecv); +} + +/* + * ECell::unrealize method + */ +static void +ecp_unrealize (ECellView *ecv) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + + e_cell_unrealize (ecp_view->child_view); + + if (E_CELL_CLASS (e_cell_popup_parent_class)->unrealize) + (* E_CELL_CLASS (e_cell_popup_parent_class)->unrealize) (ecv); +} + +/* + * ECell::draw method + */ +static void +ecp_draw (ECellView *ecv, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2) +{ + ECellPopup *ecp = E_CELL_POPUP (ecv->ecell); + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + GtkWidget *canvas; + gboolean show_popup_arrow; + + cairo_save (cr); + + canvas = GTK_WIDGET (GNOME_CANVAS_ITEM (ecv->e_table_item_view)->canvas); + + /* Display the popup arrow if we are the cursor cell, or the popup + * is shown for this cell. */ + show_popup_arrow = + e_table_model_is_cell_editable ( + ecv->e_table_model, model_col, row) && + (flags & E_CELL_CURSOR || + (ecp->popup_shown && ecp->popup_view_col == view_col + && ecp->popup_row == row + && ecp->popup_model == ((ECellView *) ecp_view)->e_table_model)); + + if (flags & E_CELL_CURSOR) + ecp->popup_arrow_shown = show_popup_arrow; + + if (show_popup_arrow) { + GtkStyleContext *style_context; + GdkRGBA color; + gint arrow_x; + gint arrow_y; + gint arrow_size; + gint midpoint_y; + + e_cell_draw ( + ecp_view->child_view, cr, model_col, + view_col, row, flags, + x1, y1, x2 - E_CELL_POPUP_ARROW_SIZE, y2); + + midpoint_y = y1 + ((y2 - y1 + 1) / 2); + + arrow_x = x2 - E_CELL_POPUP_ARROW_SIZE; + arrow_y = midpoint_y - E_CELL_POPUP_ARROW_SIZE / 2; + arrow_size = E_CELL_POPUP_ARROW_SIZE; + + style_context = gtk_widget_get_style_context (canvas); + + gtk_style_context_save (style_context); + + gtk_style_context_add_class ( + style_context, GTK_STYLE_CLASS_CELL); + + gtk_style_context_get_background_color ( + style_context, GTK_STATE_FLAG_NORMAL, &color); + + cairo_save (cr); + gdk_cairo_set_source_rgba (cr, &color); + gtk_render_background ( + style_context, cr, + (gdouble) arrow_x, + (gdouble) arrow_y, + (gdouble) arrow_size, + (gdouble) arrow_size); + cairo_restore (cr); + + arrow_x += E_CELL_POPUP_ARROW_PAD; + arrow_y += E_CELL_POPUP_ARROW_PAD; + arrow_size -= (E_CELL_POPUP_ARROW_PAD * 2); + + cairo_save (cr); + gtk_render_arrow ( + style_context, cr, G_PI, + (gdouble) arrow_x, + (gdouble) arrow_y, + (gdouble) arrow_size); + cairo_restore (cr); + + gtk_style_context_restore (style_context); + } else { + e_cell_draw ( + ecp_view->child_view, cr, model_col, + view_col, row, flags, x1, y1, x2, y2); + } + + cairo_restore (cr); +} + +/* + * ECell::event method + */ +static gint +ecp_event (ECellView *ecv, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + ECellPopup *ecp = E_CELL_POPUP (ecp_view->cell_view.ecell); + ETableItem *eti = E_TABLE_ITEM (ecv->e_table_item_view); + gint width; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (e_table_model_is_cell_editable (ecv->e_table_model, model_col, row) && + flags & E_CELL_CURSOR + && ecp->popup_arrow_shown) { + width = e_table_header_col_diff ( + eti->header, view_col, + view_col + 1); + + /* FIXME: The event coords seem to be relative to the + * text within the cell, so we have to add 4. */ + if (event->button.x + 4 >= width - E_CELL_POPUP_ARROW_SIZE) { + return e_cell_popup_do_popup (ecp_view, event, row, view_col); + } + } + break; + case GDK_KEY_PRESS: + if (e_table_model_is_cell_editable (ecv->e_table_model, model_col, row) && + event->key.state & GDK_MOD1_MASK + && event->key.keyval == GDK_KEY_Down) { + return e_cell_popup_do_popup (ecp_view, event, row, view_col); + } + break; + default: + break; + } + + return e_cell_event ( + ecp_view->child_view, event, model_col, view_col, + row, flags, actions); +} + +/* + * ECell::height method + */ +static gint +ecp_height (ECellView *ecv, + gint model_col, + gint view_col, + gint row) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + + return e_cell_height (ecp_view->child_view, model_col, view_col, row); +} + +/* + * ECellView::enter_edit method + */ +static gpointer +ecp_enter_edit (ECellView *ecv, + gint model_col, + gint view_col, + gint row) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + + return e_cell_enter_edit (ecp_view->child_view, model_col, view_col, row); +} + +/* + * ECellView::leave_edit method + */ +static void +ecp_leave_edit (ECellView *ecv, + gint model_col, + gint view_col, + gint row, + gpointer edit_context) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + + e_cell_leave_edit ( + ecp_view->child_view, model_col, view_col, row, + edit_context); +} + +static void +ecp_print (ECellView *ecv, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + + e_cell_print ( + ecp_view->child_view, context, model_col, view_col, row, + width, height); +} + +static gdouble +ecp_print_height (ECellView *ecv, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + + return e_cell_print_height ( + ecp_view->child_view, context, model_col, + view_col, row, width); +} + +static gint +ecp_max_width (ECellView *ecv, + gint model_col, + gint view_col) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecv; + + return e_cell_max_width (ecp_view->child_view, model_col, view_col); +} + +static gchar * +ecp_get_bg_color (ECellView *ecell_view, + gint row) +{ + ECellPopupView *ecp_view = (ECellPopupView *) ecell_view; + + return e_cell_get_bg_color (ecp_view->child_view, row); +} + +ECell * +e_cell_popup_get_child (ECellPopup *ecp) +{ + g_return_val_if_fail (E_IS_CELL_POPUP (ecp), NULL); + + return ecp->child; +} + +void +e_cell_popup_set_child (ECellPopup *ecp, + ECell *child) +{ + g_return_if_fail (E_IS_CELL_POPUP (ecp)); + + if (ecp->child) + g_object_unref (ecp->child); + + ecp->child = child; + g_object_ref (child); +} + +static gint +e_cell_popup_do_popup (ECellPopupView *ecp_view, + GdkEvent *event, + gint row, + gint view_col) +{ + ECellPopup *ecp = E_CELL_POPUP (ecp_view->cell_view.ecell); + gint (*popup_func) (ECellPopup *ecp, GdkEvent *event, gint row, gint view_col); + + ecp->popup_cell_view = ecp_view; + + popup_func = E_CELL_POPUP_CLASS (G_OBJECT_GET_CLASS (ecp))->popup; + + ecp->popup_view_col = view_col; + ecp->popup_row = row; + ecp->popup_model = ((ECellView *) ecp_view)->e_table_model; + + return popup_func ? popup_func (ecp, event, row, view_col) : FALSE; +} + +/* This redraws the popup cell. Only use this if you know popup_view_col and + * popup_row are valid. */ +void +e_cell_popup_queue_cell_redraw (ECellPopup *ecp) +{ + ETableItem *eti; + + eti = E_TABLE_ITEM (ecp->popup_cell_view->cell_view.e_table_item_view); + + e_table_item_redraw_range ( + eti, ecp->popup_view_col, ecp->popup_row, + ecp->popup_view_col, ecp->popup_row); +} + +void +e_cell_popup_set_shown (ECellPopup *ecp, + gboolean shown) +{ + ecp->popup_shown = shown; + e_cell_popup_queue_cell_redraw (ecp); +} diff --git a/e-util/e-cell-popup.h b/e-util/e-cell-popup.h new file mode 100644 index 0000000000..0b973d8f18 --- /dev/null +++ b/e-util/e-cell-popup.h @@ -0,0 +1,118 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * ECellPopup - an ECell used to support popup selections like a GtkCombo + * widget. It contains a child ECell, e.g. an ECellText, but when selected it + * displays an arrow on the right edge which the user can click to show a + * popup. It will support subclassing or signals so that different types of + * popup can be provided. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_POPUP_H_ +#define _E_CELL_POPUP_H_ + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-cell.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_POPUP \ + (e_cell_popup_get_type ()) +#define E_CELL_POPUP(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_POPUP, ECellPopup)) +#define E_CELL_POPUP_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_POPUP, ECellPopupClass)) +#define E_IS_CELL_POPUP(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_POPUP)) +#define E_IS_CELL_POPUP_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_POPUP)) +#define E_CELL_POPUP_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_POPUP, ECellPopupClass)) + +G_BEGIN_DECLS + +typedef struct _ECellPopup ECellPopup; +typedef struct _ECellPopupView ECellPopupView; +typedef struct _ECellPopupClass ECellPopupClass; + +struct _ECellPopup { + ECell parent; + + ECell *child; + + /* This is TRUE if the popup window is shown for the cell being + * edited. While shown we display the arrow indented. */ + gboolean popup_shown; + + /* This is TRUE if the popup arrow is shown for the cell being edited. + * This is needed to stop the first click on the cell from popping up + * the popup window. We only popup the window after we have drawn the + * arrow. */ + gboolean popup_arrow_shown; + + /* The view in which the popup is shown. */ + ECellPopupView *popup_cell_view; + + gint popup_view_col; + gint popup_row; + ETableModel *popup_model; +}; + +struct _ECellPopupView { + ECellView cell_view; + + ECellView *child_view; +}; + +struct _ECellPopupClass { + ECellClass parent_class; + + /* Virtual function for subclasses to override. */ + gint (*popup) (ECellPopup *ecp, + GdkEvent *event, + gint row, + gint view_col); +}; + +GType e_cell_popup_get_type (void) G_GNUC_CONST; +ECell * e_cell_popup_new (void); +ECell * e_cell_popup_get_child (ECellPopup *ecp); +void e_cell_popup_set_child (ECellPopup *ecp, + ECell *child); +void e_cell_popup_set_shown (ECellPopup *ecp, + gboolean shown); +void e_cell_popup_queue_cell_redraw (ECellPopup *ecp); + +G_END_DECLS + +#endif /* _E_CELL_POPUP_H_ */ diff --git a/e-util/e-cell-renderer-color.c b/e-util/e-cell-renderer-color.c new file mode 100644 index 0000000000..4bbb1318b3 --- /dev/null +++ b/e-util/e-cell-renderer-color.c @@ -0,0 +1,243 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-cell-renderer-color.c + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-cell-renderer-color.h" + +#include <string.h> +#include <glib/gi18n-lib.h> + +#define E_CELL_RENDERER_COLOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CELL_RENDERER_COLOR, ECellRendererColorPrivate)) + +enum { + PROP_0, + PROP_COLOR +}; + +struct _ECellRendererColorPrivate { + GdkColor *color; +}; + +G_DEFINE_TYPE ( + ECellRendererColor, + e_cell_renderer_color, + GTK_TYPE_CELL_RENDERER) + +static void +cell_renderer_color_get_size (GtkCellRenderer *cell, + GtkWidget *widget, + const GdkRectangle *cell_area, + gint *x_offset, + gint *y_offset, + gint *width, + gint *height) +{ + gint color_width = 16; + gint color_height = 16; + gint calc_width; + gint calc_height; + gfloat xalign; + gfloat yalign; + guint xpad; + guint ypad; + + g_object_get ( + cell, "xalign", &xalign, "yalign", &yalign, + "xpad", &xpad, "ypad", &ypad, NULL); + + calc_width = (gint) xpad * 2 + color_width; + calc_height = (gint) ypad * 2 + color_height; + + if (cell_area && color_width > 0 && color_height > 0) { + if (x_offset) { + *x_offset = (((gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) ? + (1.0 - xalign) : xalign) * + (cell_area->width - calc_width)); + *x_offset = MAX (*x_offset, 0); + } + + if (y_offset) { + *y_offset =(yalign * + (cell_area->height - calc_height)); + *y_offset = MAX (*y_offset, 0); + } + } else { + if (x_offset) *x_offset = 0; + if (y_offset) *y_offset = 0; + } + + if (width) + *width = calc_width; + + if (height) + *height = calc_height; +} + +static void +cell_renderer_color_render (GtkCellRenderer *cell, + cairo_t *cr, + GtkWidget *widget, + const GdkRectangle *background_area, + const GdkRectangle *cell_area, + GtkCellRendererState flags) +{ + ECellRendererColorPrivate *priv; + GdkRectangle pix_rect; + GdkRectangle draw_rect; + GdkRGBA rgba; + guint xpad; + guint ypad; + + priv = E_CELL_RENDERER_COLOR_GET_PRIVATE (cell); + + if (priv->color == NULL) + return; + + cell_renderer_color_get_size ( + cell, widget, cell_area, + &pix_rect.x, &pix_rect.y, + &pix_rect.width, &pix_rect.height); + + g_object_get (cell, "xpad", &xpad, "ypad", &ypad, NULL); + + pix_rect.x += cell_area->x + xpad; + pix_rect.y += cell_area->y + ypad; + pix_rect.width -= xpad * 2; + pix_rect.height -= ypad * 2; + + if (!gdk_rectangle_intersect (cell_area, &pix_rect, &draw_rect)) + return; + + rgba.red = priv->color->red / 65535.0; + rgba.green = priv->color->green / 65535.0; + rgba.blue = priv->color->blue / 65535.0; + rgba.alpha = 1.0; + + gdk_cairo_set_source_rgba (cr, &rgba); + cairo_rectangle (cr, pix_rect.x, pix_rect.y, draw_rect.width, draw_rect.height); + + cairo_fill (cr); +} + +static void +cell_renderer_color_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ECellRendererColorPrivate *priv; + + priv = E_CELL_RENDERER_COLOR_GET_PRIVATE (object); + + switch (property_id) { + case PROP_COLOR: + if (priv->color != NULL) + gdk_color_free (priv->color); + priv->color = g_value_dup_boxed (value); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +cell_renderer_color_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ECellRendererColorPrivate *priv; + + priv = E_CELL_RENDERER_COLOR_GET_PRIVATE (object); + + switch (property_id) { + case PROP_COLOR: + g_value_set_boxed (value, priv->color); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +cell_renderer_color_finalize (GObject *object) +{ + ECellRendererColorPrivate *priv; + + priv = E_CELL_RENDERER_COLOR_GET_PRIVATE (object); + + if (priv->color != NULL) + gdk_color_free (priv->color); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_cell_renderer_color_parent_class)->finalize (object); +} + +static void +e_cell_renderer_color_class_init (ECellRendererColorClass *class) +{ + GObjectClass *object_class; + GtkCellRendererClass *cell_class; + + g_type_class_add_private (class, sizeof (ECellRendererColorPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = cell_renderer_color_set_property; + object_class->get_property = cell_renderer_color_get_property; + object_class->finalize = cell_renderer_color_finalize; + + cell_class = GTK_CELL_RENDERER_CLASS (class); + cell_class->get_size = cell_renderer_color_get_size; + cell_class->render = cell_renderer_color_render; + + g_object_class_install_property ( + object_class, + PROP_COLOR, + g_param_spec_boxed ( + "color", + "Color Info", + "The color to render", + GDK_TYPE_COLOR, + G_PARAM_READWRITE)); +} + +static void +e_cell_renderer_color_init (ECellRendererColor *cellcolor) +{ + cellcolor->priv = E_CELL_RENDERER_COLOR_GET_PRIVATE (cellcolor); + + g_object_set (cellcolor, "xpad", 4, NULL); +} + +/** + * e_cell_renderer_color_new: + * + * Since: 2.22 + **/ +GtkCellRenderer * +e_cell_renderer_color_new (void) +{ + return g_object_new (E_TYPE_CELL_RENDERER_COLOR, NULL); +} diff --git a/e-util/e-cell-renderer-color.h b/e-util/e-cell-renderer-color.h new file mode 100644 index 0000000000..00dd615607 --- /dev/null +++ b/e-util/e-cell-renderer-color.h @@ -0,0 +1,79 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-cell-renderer-color.h + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_RENDERER_COLOR_H_ +#define _E_CELL_RENDERER_COLOR_H_ + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_RENDERER_COLOR \ + (e_cell_renderer_color_get_type ()) +#define E_CELL_RENDERER_COLOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_RENDERER_COLOR, ECellRendererColor)) +#define E_CELL_RENDERER_COLOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_RENDERER_COLOR, ECellRendererColorClass)) +#define E_IS_CELL_RENDERER_COLOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_RENDERER_COLOR)) +#define E_IS_CELL_RENDERER_COLOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE ((cls), E_TYPE_CELL_RENDERER_COLOR)) +#define E_CELL_RENDERER_COLOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_RENDERER_COLOR, ECellRendererColorClass)) + +G_BEGIN_DECLS + +typedef struct _ECellRendererColor ECellRendererColor; +typedef struct _ECellRendererColorClass ECellRendererColorClass; +typedef struct _ECellRendererColorPrivate ECellRendererColorPrivate; + +/** + * ECellRendererColor: + * + * Since: 2.22 + **/ +struct _ECellRendererColor { + GtkCellRenderer parent; + ECellRendererColorPrivate *priv; +}; + +struct _ECellRendererColorClass { + GtkCellRendererClass parent_class; + + /* Padding for future expansion */ + void (*_gtk_reserved1) (void); + void (*_gtk_reserved2) (void); + void (*_gtk_reserved3) (void); + void (*_gtk_reserved4) (void); +}; + +GType e_cell_renderer_color_get_type (void); +GtkCellRenderer *e_cell_renderer_color_new (void); + +G_END_DECLS + +#endif /* _E_CELL_RENDERER_COLOR_H_ */ diff --git a/e-util/e-cell-size.c b/e-util/e-cell-size.c new file mode 100644 index 0000000000..02bde88638 --- /dev/null +++ b/e-util/e-cell-size.c @@ -0,0 +1,112 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <sys/time.h> +#include <unistd.h> + +#include "e-cell-size.h" + +G_DEFINE_TYPE (ECellSize, e_cell_size, E_TYPE_CELL_TEXT) + +static gchar * +ecd_get_text (ECellText *cell, + ETableModel *model, + gint col, + gint row) +{ + gint size = GPOINTER_TO_INT (e_table_model_value_at (model, col, row)); + gfloat fsize; + + if (size < 1024) { + return g_strdup_printf ("%d bytes", size); + } else { + fsize = ((gfloat) size) / 1024.0; + if (fsize < 1024.0) { + return g_strdup_printf ("%d K", (gint) fsize); + } else { + fsize /= 1024.0; + return g_strdup_printf ("%.1f MB", fsize); + } + } +} + +static void +ecd_free_text (ECellText *cell, + gchar *text) +{ + g_free (text); +} + +static void +e_cell_size_class_init (ECellSizeClass *class) +{ + ECellTextClass *ectc = E_CELL_TEXT_CLASS (class); + + ectc->get_text = ecd_get_text; + ectc->free_text = ecd_free_text; +} + +static void +e_cell_size_init (ECellSize *e_cell_size) +{ +} + +/** + * e_cell_size_new: + * @fontname: font to be used to render on the screen + * @justify: Justification of the string in the cell. + * + * Creates a new ECell renderer that can be used to render file sizes + * that that come from the model. The value returned from the model + * is interpreted as being a time_t. + * + * The ECellSize object support a large set of properties that can be + * configured through the Gtk argument system and allows the user to + * have a finer control of the way the string is displayed. The + * arguments supported allow the control of strikeout, underline, + * bold, color and a size filter. + * + * The arguments "strikeout_column", "underline_column", "bold_column" + * and "color_column" set and return an integer that points to a + * column in the model that controls these settings. So controlling + * the way things are rendered is achieved by having special columns + * in the model that will be used to flag whether the size should be + * rendered with strikeout, underline, or bolded. In the case of the + * "color_column" argument, the column in the model is expected to + * have a string that can be parsed by gdk_color_parse(). + * + * Returns: an ECell object that can be used to render file sizes. */ +ECell * +e_cell_size_new (const gchar *fontname, + GtkJustification justify) +{ + ECellSize *ecd = g_object_new (E_TYPE_CELL_SIZE, NULL); + + e_cell_text_construct (E_CELL_TEXT (ecd), fontname, justify); + + return (ECell *) ecd; +} + diff --git a/e-util/e-cell-size.h b/e-util/e-cell-size.h new file mode 100644 index 0000000000..b04134cb03 --- /dev/null +++ b/e-util/e-cell-size.h @@ -0,0 +1,72 @@ +/* + * e-cell-size.h: Size item for e-table. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CELL_SIZE_H +#define E_CELL_SIZE_H + +#include <e-util/e-cell-text.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_SIZE \ + (e_cell_size_get_type ()) +#define E_CELL_SIZE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_SIZE, ECellSize)) +#define E_CELL_SIZE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_SIZE, ECellSizeClass)) +#define E_IS_CELL_SIZE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_SIZE)) +#define E_IS_CELL_SIZE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_SIZE)) +#define E_CELL_SIZE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_SIZE, ECellSizeClass)) + +G_BEGIN_DECLS + +typedef struct _ECellSize ECellSize; +typedef struct _ECellSizeClass ECellSizeClass; + +struct _ECellSize { + ECellText parent; +}; + +struct _ECellSizeClass { + ECellTextClass parent_class; +}; + +GType e_cell_size_get_type (void) G_GNUC_CONST; +ECell * e_cell_size_new (const gchar *fontname, + GtkJustification justify); + +G_END_DECLS + +#endif /* E_CELL_SIZE_H */ diff --git a/e-util/e-cell-text.c b/e-util/e-cell-text.c new file mode 100644 index 0000000000..577d41ccf2 --- /dev/null +++ b/e-util/e-cell-text.c @@ -0,0 +1,2810 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * Chris Lahey <clahey@ximian.com> + * + * A lot of code taken from: + * + * Text item type for GnomeCanvas widget + * + * GnomeCanvas is basically a port of the Tk toolkit's most excellent + * canvas widget. Tk is copyrighted by the Regents of the University + * of California, Sun Microsystems, and other parties. + * + * Copyright (C) 1998 The Free Software Foundation + * + * Author: Federico Mena <federico@nuclecu.unam.mx> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <ctype.h> +#include <math.h> +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-canvas.h" +#include "e-cell-text.h" +#include "e-table-item.h" +#include "e-table.h" +#include "e-text-event-processor-emacs-like.h" +#include "e-text-event-processor.h" +#include "e-text.h" +#include "e-unicode.h" + +#define d(x) +#define DO_SELECTION 1 +#define VIEW_TO_CELL(view) E_CELL_TEXT (((ECellView *)view)->ecell) + +#if d(!)0 +#define e_table_item_leave_edit_(x) (e_table_item_leave_edit((x)), g_print ("%s: e_table_item_leave_edit\n", __FUNCTION__)) +#else +#define e_table_item_leave_edit_(x) (e_table_item_leave_edit((x))) +#endif + +/* This defines a line of text */ +struct line { + gchar *text; /* Line's text UTF-8, it is a pointer into the text->text string */ + gint length; /* Line's length in BYTES */ + gint width; /* Line's width in pixels */ + gint ellipsis_length; /* Length before adding ellipsis in BYTES */ +}; + +/* Object argument IDs */ +enum { + PROP_0, + + PROP_STRIKEOUT_COLUMN, + PROP_UNDERLINE_COLUMN, + PROP_BOLD_COLUMN, + PROP_COLOR_COLUMN, + PROP_EDITABLE, + PROP_BG_COLOR_COLUMN +}; + +enum { + E_SELECTION_PRIMARY, + E_SELECTION_CLIPBOARD +}; + +/* signals */ +enum { + TEXT_INSERTED, + TEXT_DELETED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static GdkAtom clipboard_atom = GDK_NONE; + +G_DEFINE_TYPE (ECellText, e_cell_text, E_TYPE_CELL) + +#define UTF8_ATOM gdk_atom_intern ("UTF8_STRING", FALSE) + +#define TEXT_PAD 4 + +typedef struct { + gpointer lines; /* Text split into lines (private field) */ + gint num_lines; /* Number of lines of text */ + gint max_width; + gint ref_count; +} ECellTextLineBreaks; + +typedef struct _CellEdit CellEdit; + +typedef struct { + ECellView cell_view; + GdkCursor *i_cursor; + + GnomeCanvas *canvas; + + /* + * During editing. + */ + CellEdit *edit; + + gint xofs, yofs; /* This gets added to the x + and y for the cell text. */ + gdouble ellipsis_width[2]; /* The width of the ellipsis. */ +} ECellTextView; + +struct _CellEdit { + + ECellTextView *text_view; + + gint model_col, view_col, row; + gint cell_width; + + PangoLayout *layout; + + gchar *text; + + gchar *old_text; + + /* + * Where the editing is taking place + */ + + gint xofs_edit, yofs_edit; /* Offset because of editing. + This is negative compared + to the other offsets. */ + + /* This needs to be reworked a bit once we get line wrapping. */ + gint selection_start; /* Start of selection - IN BYTES */ + gint selection_end; /* End of selection - IN BYTES */ + gboolean select_by_word; /* Current selection is by word */ + + /* This section is for drag scrolling and blinking cursor. */ + /* Cursor handling. */ + gint timeout_id; /* Current timeout id for scrolling */ + GTimer *timer; /* Timer for blinking cursor and scrolling */ + + gint lastx, lasty; /* Last x and y motion events */ + gint last_state; /* Last state */ + gulong scroll_start; /* Starting time for scroll (microseconds) */ + + gint show_cursor; /* Is cursor currently shown */ + gboolean button_down; /* Is mouse button 1 down */ + + ETextEventProcessor *tep; /* Text Event Processor */ + + gboolean has_selection; /* TRUE if we have the selection */ + + guint pointer_in : 1; + guint default_cursor_shown : 1; + GtkIMContext *im_context; + gboolean need_im_reset; + gboolean im_context_signals_registered; + + guint16 preedit_length; /* length of preedit string, in bytes */ + gint preedit_pos; /* position of preedit cursor */ + + ECellActions actions; +}; + +static void e_cell_text_view_command (ETextEventProcessor *tep, ETextEventProcessorCommand *command, gpointer data); + +static void e_cell_text_view_get_selection (CellEdit *edit, GdkAtom selection, guint32 time); +static void e_cell_text_view_supply_selection (CellEdit *edit, guint time, GdkAtom selection, gchar *data, gint length); + +static void _get_tep (CellEdit *edit); + +static gint get_position_from_xy (CellEdit *edit, gint x, gint y); +static gboolean _blink_scroll_timeout (gpointer data); + +static void e_cell_text_preedit_changed_cb (GtkIMContext *context, ECellTextView *text_view); +static void e_cell_text_commit_cb (GtkIMContext *context, const gchar *str, ECellTextView *text_view); +static gboolean e_cell_text_retrieve_surrounding_cb (GtkIMContext *context, ECellTextView *text_view); +static gboolean e_cell_text_delete_surrounding_cb (GtkIMContext *context, gint offset, gint n_chars, ECellTextView *text_view); +static void _insert (ECellTextView *text_view, const gchar *string, gint value); +static void _delete_selection (ECellTextView *text_view); +static PangoAttrList * build_attr_list (ECellTextView *text_view, gint row, gint text_length); +static void update_im_cursor_location (ECellTextView *tv); + +static gchar * +ect_real_get_text (ECellText *cell, + ETableModel *model, + gint col, + gint row) +{ + return e_table_model_value_at (model, col, row); +} + +static void +ect_real_free_text (ECellText *cell, + gchar *text) +{ +} + +/* This is the default method for setting the ETableModel value based on + * the text in the ECellText. This simply uses the text as it is - it assumes + * the value in the model is a gchar *. Subclasses may parse the text into + * data structures to pass to the model. */ +static void +ect_real_set_value (ECellText *cell, + ETableModel *model, + gint col, + gint row, + const gchar *text) +{ + e_table_model_set_value_at (model, col, row, text); +} + +static void +ect_queue_redraw (ECellTextView *text_view, + gint view_col, + gint view_row) +{ + e_table_item_redraw_range ( + text_view->cell_view.e_table_item_view, + view_col, view_row, view_col, view_row); +} + +/* + * Shuts down the editing process + */ +static void +ect_stop_editing (ECellTextView *text_view, + gboolean commit) +{ + GdkWindow *window; + CellEdit *edit = text_view->edit; + gint row, view_col, model_col; + gchar *old_text, *text; + + if (!edit) + return; + + window = gtk_widget_get_window (GTK_WIDGET (text_view->canvas)); + + row = edit->row; + view_col = edit->view_col; + model_col = edit->model_col; + + old_text = edit->old_text; + text = edit->text; + if (edit->tep) + g_object_unref (edit->tep); + if (!edit->default_cursor_shown) { + gdk_window_set_cursor (window, NULL); + edit->default_cursor_shown = TRUE; + } + if (edit->timeout_id) { + g_source_remove (edit->timeout_id); + edit->timeout_id = 0; + } + if (edit->timer) { + g_timer_stop (edit->timer); + g_timer_destroy (edit->timer); + edit->timer = NULL; + } + + g_signal_handlers_disconnect_matched ( + edit->im_context, + G_SIGNAL_MATCH_DATA, 0, 0, + NULL, NULL, text_view); + + if (edit->layout) + g_object_unref (edit->layout); + + g_free (edit); + + text_view->edit = NULL; + if (commit) { + /* + * Accept the currently edited text. if it's the same as what's in the cell, do nothing. + */ + ECellView *ecell_view = (ECellView *) text_view; + ECellText *ect = (ECellText *) ecell_view->ecell; + + if (strcmp (old_text, text)) { + e_cell_text_set_value ( + ect, ecell_view->e_table_model, + model_col, row, text); + } + } + g_free (text); + g_free (old_text); + + ect_queue_redraw (text_view, view_col, row); +} + +/* + * Cancels the edits + */ +static void +ect_cancel_edit (ECellTextView *text_view) +{ + ect_stop_editing (text_view, FALSE); + e_table_item_leave_edit_ (text_view->cell_view.e_table_item_view); +} + +/* + * ECell::new_view method + */ +static ECellView * +ect_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view) +{ + ECellTextView *text_view = g_new0 (ECellTextView, 1); + GnomeCanvas *canvas = GNOME_CANVAS_ITEM (e_table_item_view)->canvas; + + text_view->cell_view.ecell = ecell; + text_view->cell_view.e_table_model = table_model; + text_view->cell_view.e_table_item_view = e_table_item_view; + text_view->cell_view.kill_view_cb = NULL; + text_view->cell_view.kill_view_cb_data = NULL; + + text_view->canvas = canvas; + + text_view->xofs = 0.0; + text_view->yofs = 0.0; + + return (ECellView *) text_view; +} + +/* + * ECell::kill_view method + */ +static void +ect_kill_view (ECellView *ecv) +{ + ECellTextView *text_view = (ECellTextView *) ecv; + + if (text_view->cell_view.kill_view_cb) + (text_view->cell_view.kill_view_cb)(ecv, text_view->cell_view.kill_view_cb_data); + + if (text_view->cell_view.kill_view_cb_data) + g_list_free (text_view->cell_view.kill_view_cb_data); + + g_free (text_view); +} + +/* + * ECell::realize method + */ +static void +ect_realize (ECellView *ecell_view) +{ + ECellTextView *text_view = (ECellTextView *) ecell_view; + + text_view->i_cursor = gdk_cursor_new (GDK_XTERM); + + if (E_CELL_CLASS (e_cell_text_parent_class)->realize) + (* E_CELL_CLASS (e_cell_text_parent_class)->realize) (ecell_view); +} + +/* + * ECell::unrealize method + */ +static void +ect_unrealize (ECellView *ecv) +{ + ECellTextView *text_view = (ECellTextView *) ecv; + + if (text_view->edit) { + ect_cancel_edit (text_view); + } + + g_object_unref (text_view->i_cursor); + + if (E_CELL_CLASS (e_cell_text_parent_class)->unrealize) + (* E_CELL_CLASS (e_cell_text_parent_class)->unrealize) (ecv); + +} + +static PangoAttrList * +build_attr_list (ECellTextView *text_view, + gint row, + gint text_length) +{ + + ECellView *ecell_view = (ECellView *) text_view; + ECellText *ect = E_CELL_TEXT (ecell_view->ecell); + PangoAttrList *attrs = pango_attr_list_new (); + gboolean bold, strikeout, underline; + + bold = ect->bold_column >= 0 && + row >= 0 && + e_table_model_value_at (ecell_view->e_table_model, ect->bold_column, row); + strikeout = ect->strikeout_column >= 0 && + row >= 0 && + e_table_model_value_at (ecell_view->e_table_model, ect->strikeout_column, row); + underline = ect->underline_column >= 0 && + row >= 0 && + e_table_model_value_at (ecell_view->e_table_model, ect->underline_column, row); + + if (bold || strikeout || underline) { + if (bold) { + PangoAttribute *attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD); + attr->start_index = 0; + attr->end_index = text_length; + + pango_attr_list_insert_before (attrs, attr); + } + if (strikeout) { + PangoAttribute *attr = pango_attr_strikethrough_new (TRUE); + attr->start_index = 0; + attr->end_index = text_length; + + pango_attr_list_insert_before (attrs, attr); + } + if (underline) { + PangoAttribute *attr = pango_attr_underline_new (TRUE); + attr->start_index = 0; + attr->end_index = text_length; + + pango_attr_list_insert_before (attrs, attr); + } + } + return attrs; +} + +static PangoLayout * +layout_with_preedit (ECellTextView *text_view, + gint row, + const gchar *text, + gint width) +{ + CellEdit *edit = text_view->edit; + PangoAttrList *attrs; + PangoLayout *layout; + GString *tmp_string = g_string_new (NULL); + PangoAttrList *preedit_attrs = NULL; + gchar *preedit_string = NULL; + gint preedit_length = 0; + gint text_length = strlen (text); + gint mlen = MIN (edit->selection_start,text_length); + + gtk_im_context_get_preedit_string ( + edit->im_context, + &preedit_string,&preedit_attrs, + NULL); + preedit_length = edit->preedit_length = strlen (preedit_string);; + + layout = edit->layout; + + g_string_prepend_len (tmp_string, text,text_length); + + if (preedit_length) { + + /* mlen is the text_length in bytes, not chars + * check whether we are not inserting into + * the middle of a utf8 character + */ + + if (mlen < text_length) { + if (!g_utf8_validate (text + mlen, -1, NULL)) { + gchar *tc; + tc = g_utf8_find_next_char (text + mlen,NULL); + if (tc) { + mlen = (gint) (tc - text); + } + } + } + + g_string_insert (tmp_string, mlen, preedit_string); + } + + pango_layout_set_text (layout, tmp_string->str, tmp_string->len); + + attrs = (PangoAttrList *) build_attr_list (text_view, row, text_length); + + if (preedit_length) + pango_attr_list_splice (attrs, preedit_attrs, mlen, preedit_length); + pango_layout_set_attributes (layout, attrs); + g_string_free (tmp_string, TRUE); + if (preedit_string) + g_free (preedit_string); + if (preedit_attrs) + pango_attr_list_unref (preedit_attrs); + pango_attr_list_unref (attrs); + + update_im_cursor_location (text_view); + + return layout; +} + +static PangoLayout * +build_layout (ECellTextView *text_view, + gint row, + const gchar *text, + gint width) +{ + ECellView *ecell_view = (ECellView *) text_view; + ECellText *ect = E_CELL_TEXT (ecell_view->ecell); + PangoAttrList *attrs; + PangoLayout *layout; + + layout = gtk_widget_create_pango_layout (GTK_WIDGET (((GnomeCanvasItem *) ecell_view->e_table_item_view)->canvas), text); + + attrs = (PangoAttrList *) build_attr_list (text_view, row, text ? strlen (text) : 0); + + pango_layout_set_attributes (layout, attrs); + pango_attr_list_unref (attrs); + + if (text_view->edit || width <= 0) + return layout; + + if (ect->font_name) + { + PangoFontDescription *desc = NULL, *fixed_desc = NULL; + gchar *fixed_family = NULL; + gint fixed_size = 0; + gboolean fixed_points = TRUE; + + fixed_desc = pango_font_description_from_string (ect->font_name); + if (fixed_desc) { + fixed_family = (gchar *) pango_font_description_get_family (fixed_desc); + fixed_size = pango_font_description_get_size (fixed_desc); + fixed_points = !pango_font_description_get_size_is_absolute (fixed_desc); + } + + desc = pango_font_description_copy (gtk_widget_get_style (GTK_WIDGET (((GnomeCanvasItem *) ecell_view->e_table_item_view)->canvas))->font_desc); + pango_font_description_set_family (desc, fixed_family); + if (fixed_points) + pango_font_description_set_size (desc, fixed_size); + else + pango_font_description_set_absolute_size (desc, fixed_size); +/* pango_font_description_set_style (desc, PANGO_STYLE_OBLIQUE); */ + pango_layout_set_font_description (layout, desc); + pango_font_description_free (desc); + pango_font_description_free (fixed_desc); + } + + pango_layout_set_width (layout, width * PANGO_SCALE); + pango_layout_set_wrap (layout, PANGO_WRAP_WORD_CHAR); + + pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_END); + pango_layout_set_height (layout, 0); + + switch (ect->justify) { + case GTK_JUSTIFY_RIGHT: + pango_layout_set_alignment (layout, PANGO_ALIGN_RIGHT); + break; + case GTK_JUSTIFY_CENTER: + pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER); + break; + case GTK_JUSTIFY_LEFT: + default: + break; + } + + return layout; +} + +static PangoLayout * +generate_layout (ECellTextView *text_view, + gint model_col, + gint view_col, + gint row, + gint width) +{ + ECellView *ecell_view = (ECellView *) text_view; + ECellText *ect = E_CELL_TEXT (ecell_view->ecell); + PangoLayout *layout; + CellEdit *edit = text_view->edit; + + if (edit && edit->layout && edit->model_col == model_col && edit->row == row) { + g_object_ref (edit->layout); + return edit->layout; + } + + if (row >= 0) { + gchar *temp = e_cell_text_get_text (ect, ecell_view->e_table_model, model_col, row); + layout = build_layout (text_view, row, temp ? temp : "?", width); + e_cell_text_free_text (ect, temp); + } else + layout = build_layout (text_view, row, "Mumbo Jumbo", width); + + return layout; +} + +static void +draw_cursor (cairo_t *cr, + gint x1, + gint y1, + PangoRectangle rect) +{ + gdouble scaled_x; + gdouble scaled_y; + gdouble scaled_height; + + /* Pango stores each cursor position as a zero-width rectangle. */ + scaled_x = x1 + ((gdouble) rect.x) / PANGO_SCALE; + scaled_y = y1 + ((gdouble) rect.y) / PANGO_SCALE; + scaled_height = ((gdouble) rect.height) / PANGO_SCALE; + + /* Adding 0.5 to scaled_x gives a sharp, one-pixel line. */ + cairo_move_to (cr, scaled_x + 0.5, scaled_y); + cairo_line_to (cr, scaled_x + 0.5, scaled_y + scaled_height); + cairo_set_line_width (cr, 1); + cairo_stroke (cr); +} + +static gboolean +show_pango_rectangle (CellEdit *edit, + PangoRectangle rect) +{ + gint x1 = rect.x / PANGO_SCALE; + gint x2 = (rect.x + rect.width) / PANGO_SCALE; +#if 0 + gint y1 = rect.y / PANGO_SCALE; + gint y2 = (rect.y + rect.height) / PANGO_SCALE; +#endif + + gint new_xofs_edit = edit->xofs_edit; + gint new_yofs_edit = edit->yofs_edit; + + if (x1 < new_xofs_edit) + new_xofs_edit = x1; + if (2 + x2 - edit->cell_width > new_xofs_edit) + new_xofs_edit = 2 + x2 - edit->cell_width; + if (new_xofs_edit < 0) + new_xofs_edit = 0; + +#if 0 + if (y1 < new_yofs_edit) + new_yofs_edit = y1; + if (2 + y2 - edit->cell_height > new_yofs_edit) + new_yofs_edit = 2 + y2 - edit->cell_height; + if (new_yofs_edit < 0) + new_yofs_edit = 0; +#endif + + if (new_xofs_edit != edit->xofs_edit || + new_yofs_edit != edit->yofs_edit) { + edit->xofs_edit = new_xofs_edit; + edit->yofs_edit = new_yofs_edit; + return TRUE; + } + + return FALSE; +} + +static gint +get_vertical_spacing (GtkWidget *canvas) +{ + GtkWidget *widget; + gint vspacing = 0; + + g_return_val_if_fail (E_IS_CANVAS (canvas), 3); + + /* The parent should be either an ETable or ETree. */ + widget = gtk_widget_get_parent (canvas); + + gtk_widget_style_get (widget, "vertical-spacing", &vspacing, NULL); + + return vspacing; +} + +/* + * ECell::draw method + */ +static void +ect_draw (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2) +{ + PangoLayout *layout; + ECellTextView *text_view = (ECellTextView *) ecell_view; + ECellText *ect = E_CELL_TEXT (ecell_view->ecell); + CellEdit *edit = text_view->edit; + gboolean selected; + GtkWidget *canvas = GTK_WIDGET (text_view->canvas); + GtkStyle *style; + gint x_origin, y_origin, vspacing; + + cairo_save (cr); + style = gtk_widget_get_style (canvas); + + selected = flags & E_CELL_SELECTED; + + if (selected) { + if (gtk_widget_has_focus (canvas)) + gdk_cairo_set_source_color (cr, &style->fg[GTK_STATE_SELECTED]); + else + gdk_cairo_set_source_color (cr, &style->fg[GTK_STATE_ACTIVE]); + } else { + gdk_cairo_set_source_color (cr, &style->text[GTK_STATE_NORMAL]); + + if (ect->color_column != -1) { + gchar *color_spec; + GdkColor color; + + color_spec = e_table_model_value_at ( + ecell_view->e_table_model, + ect->color_column, row); + if (color_spec && gdk_color_parse (color_spec, &color)) + gdk_cairo_set_source_color (cr, &color); + } + } + + vspacing = get_vertical_spacing (canvas); + + x1 += 4; + y1 += vspacing; + x2 -= 4; + y2 -= vspacing; + + x_origin = x1 + ect->x + text_view->xofs - (edit ? edit->xofs_edit : 0); + y_origin = y1 + ect->y + text_view->yofs - (edit ? edit->yofs_edit : 0); + + cairo_rectangle (cr, x1, y1, x2 - x1, y2 - y1); + cairo_clip (cr); + + layout = generate_layout (text_view, model_col, view_col, row, x2 - x1); + + if (edit && edit->view_col == view_col && edit->row == row) { + layout = layout_with_preedit (text_view, row, edit->text ? edit->text : "?", x2 - x1); + } + + cairo_move_to (cr, x_origin, y_origin); + pango_cairo_show_layout (cr, layout); + + if (edit && edit->view_col == view_col && edit->row == row) { + if (edit->selection_start != edit->selection_end) { + cairo_region_t *clip_region; + gint indices[2]; + GtkStateType state; + + state = edit->has_selection ? GTK_STATE_SELECTED : GTK_STATE_ACTIVE; + + indices[0] = MIN (edit->selection_start, edit->selection_end); + indices[1] = MAX (edit->selection_start, edit->selection_end); + + clip_region = gdk_pango_layout_get_clip_region ( + layout, x_origin, y_origin, indices, 1); + gdk_cairo_region (cr, clip_region); + cairo_clip (cr); + cairo_region_destroy (clip_region); + + gdk_cairo_set_source_color (cr, &style->base[state]); + cairo_paint (cr); + + gdk_cairo_set_source_color (cr, &style->text[state]); + cairo_move_to (cr, x_origin, y_origin); + pango_cairo_show_layout (cr, layout); + } else { + if (edit->show_cursor) { + PangoRectangle strong_pos, weak_pos; + pango_layout_get_cursor_pos (layout, edit->selection_start + edit->preedit_length, &strong_pos, &weak_pos); + + draw_cursor (cr, x_origin, y_origin, strong_pos); + if (strong_pos.x != weak_pos.x || + strong_pos.y != weak_pos.y || + strong_pos.width != weak_pos.width || + strong_pos.height != weak_pos.height) + draw_cursor (cr, x_origin, y_origin, weak_pos); + } + } + } + + g_object_unref (layout); + cairo_restore (cr); +} + +/* + * Get the background color + */ +static gchar * +ect_get_bg_color (ECellView *ecell_view, + gint row) +{ + ECellText *ect = E_CELL_TEXT (ecell_view->ecell); + gchar *color_spec; + + if (ect->bg_color_column == -1) + return NULL; + + color_spec = e_table_model_value_at ( + ecell_view->e_table_model, + ect->bg_color_column, row); + + return color_spec; +} + +/* + * Selects the entire string + */ + +static void +ect_edit_select_all (ECellTextView *text_view) +{ + g_return_if_fail (text_view->edit); + + text_view->edit->selection_start = 0; + text_view->edit->selection_end = strlen (text_view->edit->text); +} + +static gboolean +key_begins_editing (GdkEventKey *event) +{ + if (event->length == 0) + return FALSE; + + return TRUE; +} + +/* + * ECell::event method + */ +static gint +ect_event (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions) +{ + ECellTextView *text_view = (ECellTextView *) ecell_view; + ETextEventProcessorEvent e_tep_event; + gboolean edit_display = FALSE; + gint preedit_len; + CellEdit *edit = text_view->edit; + GtkWidget *canvas = GTK_WIDGET (text_view->canvas); + gint return_val = 0; + d (gboolean press = FALSE); + + if (!(flags & E_CELL_EDITING)) + return 0; + + if (edit && !edit->preedit_length && flags & E_CELL_PREEDIT) + return 1; + + if (edit && edit->view_col == view_col && edit->row == row) { + edit_display = TRUE; + } + + e_tep_event.type = event->type; + switch (event->type) { + case GDK_FOCUS_CHANGE: + break; + case GDK_KEY_PRESS: /* Fall Through */ + if (edit_display) { + edit->show_cursor = FALSE; + } else { + ect_stop_editing (text_view, TRUE); + } + return_val = TRUE; + /* Fallthrough */ + case GDK_KEY_RELEASE: + preedit_len = edit_display ? edit->preedit_length : 0; + if (edit_display && edit->im_context && + gtk_im_context_filter_keypress (\ + edit->im_context, + (GdkEventKey *) event)) { + + edit->need_im_reset = TRUE; + if (preedit_len && flags & E_CELL_PREEDIT) + return 0; + else + return 1; + } + + if (event->key.keyval == GDK_KEY_Escape) { + /* if not changed, then pass this even to parent */ + return_val = text_view->edit != NULL && text_view->edit->text && text_view->edit->old_text && 0 != strcmp (text_view->edit->text, text_view->edit->old_text); + ect_cancel_edit (text_view); + break; + } + + if ((!edit_display) && + e_table_model_is_cell_editable (ecell_view->e_table_model, model_col, row) && + key_begins_editing (&event->key)) { + e_table_item_enter_edit (text_view->cell_view.e_table_item_view, view_col, row); + ect_edit_select_all (text_view); + edit = text_view->edit; + edit_display = TRUE; + } + if (edit_display) { + GdkEventKey key = event->key; + if (key.type == GDK_KEY_PRESS && + (key.keyval == GDK_KEY_KP_Enter || key.keyval == GDK_KEY_Return)) { + /* stop editing when it's only GDK_KEY_PRESS event */ + e_table_item_leave_edit_ (text_view->cell_view.e_table_item_view); + } else { + e_tep_event.key.time = key.time; + e_tep_event.key.state = key.state; + e_tep_event.key.keyval = key.keyval; + + /* This is probably ugly hack, but we have to handle UTF-8 input somehow */ +#if 0 + e_tep_event.key.length = key.length; + e_tep_event.key.string = key.string; +#else + e_tep_event.key.string = e_utf8_from_gtk_event_key (canvas, key.keyval, key.string); + if (e_tep_event.key.string != NULL) { + e_tep_event.key.length = strlen (e_tep_event.key.string); + } else { + e_tep_event.key.length = 0; + } +#endif + _get_tep (edit); + return_val = e_text_event_processor_handle_event (edit->tep, &e_tep_event); + if (e_tep_event.key.string) + g_free ((gpointer) e_tep_event.key.string); + break; + } + } + + break; + case GDK_BUTTON_PRESS: /* Fall Through */ + d (press = TRUE); + case GDK_BUTTON_RELEASE: + d (g_print ("%s: %s\n", __FUNCTION__, press ? "GDK_BUTTON_PRESS" : "GDK_BUTTON_RELEASE")); + event->button.x -= 4; + event->button.y -= 1; + if ((!edit_display) + && e_table_model_is_cell_editable (ecell_view->e_table_model, model_col, row) + && event->type == GDK_BUTTON_RELEASE + && event->button.button == 1) { + GdkEventButton button = event->button; + + e_table_item_enter_edit (text_view->cell_view.e_table_item_view, view_col, row); + edit = text_view->edit; + edit_display = TRUE; + + e_tep_event.button.type = GDK_BUTTON_PRESS; + e_tep_event.button.time = button.time; + e_tep_event.button.state = button.state; + e_tep_event.button.button = button.button; + e_tep_event.button.position = get_position_from_xy (edit, event->button.x, event->button.y); + e_tep_event.button.device = + gdk_event_get_device (event); + _get_tep (edit); + edit->actions = 0; + return_val = e_text_event_processor_handle_event ( + edit->tep, &e_tep_event); + *actions = edit->actions; + if (event->button.button == 1) { + if (event->type == GDK_BUTTON_PRESS) + edit->button_down = TRUE; + else + edit->button_down = FALSE; + } + edit->lastx = button.x; + edit->lasty = button.y; + edit->last_state = button.state; + + e_tep_event.button.type = GDK_BUTTON_RELEASE; + } + if (edit_display) { + GdkEventButton button = event->button; + e_tep_event.button.time = button.time; + e_tep_event.button.state = button.state; + e_tep_event.button.button = button.button; + e_tep_event.button.position = get_position_from_xy (edit, event->button.x, event->button.y); + e_tep_event.button.device = + gdk_event_get_device (event); + _get_tep (edit); + edit->actions = 0; + return_val = e_text_event_processor_handle_event ( + edit->tep, &e_tep_event); + *actions = edit->actions; + if (event->button.button == 1) { + if (event->type == GDK_BUTTON_PRESS) + edit->button_down = TRUE; + else + edit->button_down = FALSE; + } + edit->lastx = button.x; + edit->lasty = button.y; + edit->last_state = button.state; + } + break; + case GDK_MOTION_NOTIFY: + event->motion.x -= 4; + event->motion.y -= 1; + if (edit_display) { + GdkEventMotion motion = event->motion; + e_tep_event.motion.time = motion.time; + e_tep_event.motion.state = motion.state; + e_tep_event.motion.position = get_position_from_xy (edit, event->motion.x, event->motion.y); + _get_tep (edit); + edit->actions = 0; + return_val = e_text_event_processor_handle_event ( + edit->tep, &e_tep_event); + *actions = edit->actions; + edit->lastx = motion.x; + edit->lasty = motion.y; + edit->last_state = motion.state; + } + break; + case GDK_ENTER_NOTIFY: +#if 0 + edit->pointer_in = TRUE; +#endif + if (edit_display) { + if (edit->default_cursor_shown) { + GdkWindow *window; + + window = gtk_widget_get_window (canvas); + gdk_window_set_cursor (window, text_view->i_cursor); + edit->default_cursor_shown = FALSE; + } + } + break; + case GDK_LEAVE_NOTIFY: +#if 0 + text_view->pointer_in = FALSE; +#endif + if (edit_display) { + if (!edit->default_cursor_shown) { + GdkWindow *window; + + window = gtk_widget_get_window (canvas); + gdk_window_set_cursor (window, NULL); + edit->default_cursor_shown = TRUE; + } + } + break; + default: + break; + } + + return return_val; +} + +/* + * ECell::height method + */ +static gint +ect_height (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + ECellTextView *text_view = (ECellTextView *) ecell_view; + gint height; + PangoLayout *layout; + + layout = generate_layout (text_view, model_col, view_col, row, 0); + pango_layout_get_pixel_size (layout, NULL, &height); + g_object_unref (layout); + return height + (get_vertical_spacing (GTK_WIDGET (text_view->canvas)) * 2); +} + +/* + * ECellView::enter_edit method + */ +static gpointer +ect_enter_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + ECellTextView *text_view = (ECellTextView *) ecell_view; + CellEdit *edit; + ECellText *ect = E_CELL_TEXT (ecell_view->ecell); + gchar *temp; + + edit = g_new0 (CellEdit, 1); + text_view->edit = edit; + + edit->im_context = E_CANVAS (text_view->canvas)->im_context; + edit->need_im_reset = FALSE; + edit->im_context_signals_registered = FALSE; + edit->view_col = -1; + edit->model_col = -1; + edit->row = -1; + + edit->text_view = text_view; + edit->model_col = model_col; + edit->view_col = view_col; + edit->row = row; + edit->cell_width = e_table_header_get_column ( + ((ETableItem *) ecell_view->e_table_item_view)->header, + view_col)->width - 8; + + edit->layout = generate_layout (text_view, model_col, view_col, row, edit->cell_width); + + edit->xofs_edit = 0.0; + edit->yofs_edit = 0.0; + + edit->selection_start = 0; + edit->selection_end = 0; + edit->select_by_word = FALSE; + + edit->timeout_id = g_timeout_add (10, _blink_scroll_timeout, text_view); + edit->timer = g_timer_new (); + g_timer_elapsed (edit->timer, &(edit->scroll_start)); + g_timer_start (edit->timer); + + edit->lastx = 0; + edit->lasty = 0; + edit->last_state = 0; + + edit->scroll_start = 0; + edit->show_cursor = TRUE; + edit->button_down = FALSE; + + edit->tep = NULL; + + edit->has_selection = FALSE; + + edit->pointer_in = FALSE; + edit->default_cursor_shown = TRUE; + + temp = e_cell_text_get_text (ect, ecell_view->e_table_model, model_col, row); + edit->old_text = g_strdup (temp); + e_cell_text_free_text (ect, temp); + edit->text = g_strdup (edit->old_text); + + if (edit->im_context) { + gtk_im_context_reset (edit->im_context); + if (!edit->im_context_signals_registered) { + g_signal_connect ( + edit->im_context, "preedit_changed", + G_CALLBACK (e_cell_text_preedit_changed_cb), + text_view); + g_signal_connect ( + edit->im_context, "commit", + G_CALLBACK (e_cell_text_commit_cb), + text_view); + g_signal_connect ( + edit->im_context, "retrieve_surrounding", + G_CALLBACK (e_cell_text_retrieve_surrounding_cb), + text_view); + g_signal_connect ( + edit->im_context, "delete_surrounding", + G_CALLBACK (e_cell_text_delete_surrounding_cb), + text_view); + + edit->im_context_signals_registered = TRUE; + } + gtk_im_context_focus_in (edit->im_context); + } + +#if 0 + if (edit->pointer_in) { + if (edit->default_cursor_shown) { + gdk_window_set_cursor (GTK_WIDGET (item->canvas)->window, text_view->i_cursor); + edit->default_cursor_shown = FALSE; + } + } +#endif + ect_queue_redraw (text_view, view_col, row); + + return NULL; +} + +/* + * ECellView::leave_edit method + */ +static void +ect_leave_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context) +{ + ECellTextView *text_view = (ECellTextView *) ecell_view; + CellEdit *edit = text_view->edit; + + if (edit) { + if (edit->im_context) { + gtk_im_context_focus_out (edit->im_context); + + if (edit->im_context_signals_registered) { + g_signal_handlers_disconnect_matched (edit->im_context, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, edit); + edit->im_context_signals_registered = FALSE; + } + } + ect_stop_editing (text_view, TRUE); + } else { + /* + * We did invoke this leave edit internally + */ + } +} + +/* + * ECellView::save_state method + */ +static gpointer +ect_save_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context) +{ + ECellTextView *text_view = (ECellTextView *) ecell_view; + CellEdit *edit = text_view->edit; + + gint *save_state = g_new (int, 2); + + save_state[0] = edit->selection_start; + save_state[1] = edit->selection_end; + return save_state; +} + +/* + * ECellView::load_state method + */ +static void +ect_load_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context, + gpointer save_state) +{ + ECellTextView *text_view = (ECellTextView *) ecell_view; + CellEdit *edit = text_view->edit; + gint length; + gint *selection = save_state; + + length = strlen (edit->text); + + edit->selection_start = MIN (selection[0], length); + edit->selection_end = MIN (selection[1], length); + + ect_queue_redraw (text_view, view_col, row); +} + +/* + * ECellView::free_state method + */ +static void +ect_free_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer save_state) +{ + g_free (save_state); +} + +static void +get_font_size (PangoLayout *layout, + PangoFontDescription *font, + const gchar *text, + gdouble *width, + gdouble *height) +{ + gint w; + gint h; + + g_return_if_fail (layout != NULL); + pango_layout_set_font_description (layout, font); + pango_layout_set_text (layout, text, -1); + pango_layout_set_width (layout, -1); + pango_layout_set_indent (layout, 0); + + pango_layout_get_size (layout, &w, &h); + + *width = (gdouble)w/(gdouble)PANGO_SCALE; + *height = (gdouble)h/(gdouble)PANGO_SCALE; +} + +static void +ect_print (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height) +{ + PangoFontDescription *font_des; + PangoLayout *layout; + PangoContext *pango_context; + PangoFontMetrics *font_metrics; + ECellText *ect = E_CELL_TEXT (ecell_view->ecell); + ECellTextView *ectView = (ECellTextView *) ecell_view; + GtkWidget *canvas = GTK_WIDGET (ectView->canvas); + GtkStyle *style; + PangoDirection dir; + gboolean strikeout, underline; + cairo_t *cr; + gchar *string; + gdouble ty, ly, text_width = 0.0, text_height = 0.0; + + cr = gtk_print_context_get_cairo_context (context); + string = e_cell_text_get_text (ect, ecell_view->e_table_model, model_col, row); + + cairo_save (cr); + layout = gtk_print_context_create_pango_layout (context); + font_des = pango_font_description_from_string ("sans 10"); /* fix me font hardcoded */ + pango_layout_set_font_description (layout, font_des); + + pango_layout_set_text (layout, string, -1); + get_font_size (layout, font_des, string, &text_width, &text_height); + + cairo_move_to (cr, 2, 2); + cairo_rectangle (cr, 2, 2, width + 2, height + 2); + cairo_clip (cr); + + style = gtk_widget_get_style (canvas); + pango_context = gtk_widget_get_pango_context (canvas); + font_metrics = pango_context_get_metrics ( + pango_context, style->font_desc, + pango_context_get_language (pango_context)); + ty = (gdouble)(text_height - + pango_font_metrics_get_ascent (font_metrics) - + pango_font_metrics_get_descent (font_metrics)) / 2.0 /(gdouble) PANGO_SCALE; + + strikeout = ect->strikeout_column >= 0 && row >= 0 && + e_table_model_value_at (ecell_view->e_table_model, ect->strikeout_column, row); + underline = ect->underline_column >= 0 && row >= 0 && + e_table_model_value_at (ecell_view->e_table_model, ect->underline_column, row); + + dir = pango_find_base_dir (string, strlen (string)); + + if (underline) { + ly = ty + (gdouble) pango_font_metrics_get_underline_position (font_metrics) / (gdouble) PANGO_SCALE; + cairo_new_path (cr); + if (dir == PANGO_DIRECTION_RTL) { + cairo_move_to (cr, width - 2, ly + text_height + 6); + cairo_line_to (cr, MAX (width - 2 - text_width, 2), ly + text_height + 6); + } + else { + cairo_move_to (cr, 2, ly + text_height + 6); + cairo_line_to (cr, MIN (2 + text_width, width - 2), ly + text_height + 6); + } + cairo_set_line_width (cr, (gdouble) pango_font_metrics_get_underline_thickness (font_metrics) / (gdouble) PANGO_SCALE); + cairo_stroke (cr); + } + + if (strikeout) { + ly = ty + (gdouble) pango_font_metrics_get_strikethrough_position (font_metrics) / (gdouble) PANGO_SCALE; + cairo_new_path (cr); + if (dir == PANGO_DIRECTION_RTL) { + cairo_move_to (cr, width - 2, ly + text_height + 6); + cairo_line_to (cr, MAX (width - 2 - text_width, 2), ly + text_height + 6); + } + else { + cairo_move_to (cr, 2, ly + text_height + 6); + cairo_line_to (cr, MIN (2 + text_width, width - 2), ly + text_height + 6); + } + cairo_set_line_width (cr,(gdouble) pango_font_metrics_get_strikethrough_thickness (font_metrics) / (gdouble) PANGO_SCALE); + + cairo_stroke (cr); + } + + cairo_move_to (cr, 2, text_height- 5); + pango_layout_set_width (layout, (width - 4) * PANGO_SCALE); + pango_layout_set_wrap (layout, PANGO_WRAP_CHAR); + pango_cairo_show_layout (cr, layout); + cairo_restore (cr); + + pango_font_description_free (font_des); + g_object_unref (layout); + e_cell_text_free_text (ect, string); +} + +static gdouble +ect_print_height (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width) +{ + /* + * Font size is 16 by default. To leave some margin for cell + * text area, 2 for footer, 2 for header, actual print height + * should be 16 + 4. + * Height of some special font is much higher than others, + * such as Arabic. So leave some more margin for cell. + */ + PangoFontDescription *font_des; + PangoLayout *layout; + ECellText *ect = E_CELL_TEXT (ecell_view->ecell); + gchar *string; + gdouble text_width = 0.0, text_height = 0.0; + gint lines = 1; + + string = e_cell_text_get_text (ect, ecell_view->e_table_model, model_col, row); + + layout = gtk_print_context_create_pango_layout (context); + font_des = pango_font_description_from_string ("sans 10"); /* fix me font hardcoded */ + pango_layout_set_font_description (layout, font_des); + + pango_layout_set_text (layout, string, -1); + get_font_size (layout, font_des, string, &text_width, &text_height); + /* Checking if the text width goes beyond the column width to increase the + * number of lines. + */ + if (text_width > width - 4) + lines = (text_width / (width - 4)) + 1; + return 16 *lines + 8; +} + +static gint +ect_max_width (ECellView *ecell_view, + gint model_col, + gint view_col) +{ + /* New ECellText */ + ECellTextView *text_view = (ECellTextView *) ecell_view; + gint row; + gint number_of_rows; + gint max_width = 0; + + number_of_rows = e_table_model_row_count (ecell_view->e_table_model); + + for (row = 0; row < number_of_rows; row++) { + PangoLayout *layout = generate_layout (text_view, model_col, view_col, row, 0); + gint width; + + pango_layout_get_pixel_size (layout, &width, NULL); + + max_width = MAX (max_width, width); + g_object_unref (layout); + } + + return max_width + 8; +} + +static gint +ect_max_width_by_row (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + /* New ECellText */ + ECellTextView *text_view = (ECellTextView *) ecell_view; + gint width; + PangoLayout *layout; + + if (row >= e_table_model_row_count (ecell_view->e_table_model)) + return 0; + + layout = generate_layout (text_view, model_col, view_col, row, 0); + pango_layout_get_pixel_size (layout, &width, NULL); + g_object_unref (layout); + + return width + 8; +} + +static void +ect_finalize (GObject *object) +{ + ECellText *ect = E_CELL_TEXT (object); + + g_free (ect->font_name); + + G_OBJECT_CLASS (e_cell_text_parent_class)->finalize (object); +} + +/* Set_arg handler for the text item */ +static void +ect_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ECellText *text; + + text = E_CELL_TEXT (object); + + switch (property_id) { + case PROP_STRIKEOUT_COLUMN: + text->strikeout_column = g_value_get_int (value); + break; + + case PROP_UNDERLINE_COLUMN: + text->underline_column = g_value_get_int (value); + break; + + case PROP_BOLD_COLUMN: + text->bold_column = g_value_get_int (value); + break; + + case PROP_COLOR_COLUMN: + text->color_column = g_value_get_int (value); + break; + + case PROP_EDITABLE: + text->editable = g_value_get_boolean (value); + break; + + case PROP_BG_COLOR_COLUMN: + text->bg_color_column = g_value_get_int (value); + break; + + default: + return; + } +} + +/* Get_arg handler for the text item */ +static void +ect_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ECellText *text; + + text = E_CELL_TEXT (object); + + switch (property_id) { + case PROP_STRIKEOUT_COLUMN: + g_value_set_int (value, text->strikeout_column); + break; + + case PROP_UNDERLINE_COLUMN: + g_value_set_int (value, text->underline_column); + break; + + case PROP_BOLD_COLUMN: + g_value_set_int (value, text->bold_column); + break; + + case PROP_COLOR_COLUMN: + g_value_set_int (value, text->color_column); + break; + + case PROP_EDITABLE: + g_value_set_boolean (value, text->editable); + break; + + case PROP_BG_COLOR_COLUMN: + g_value_set_int (value, text->bg_color_column); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static gchar *ellipsis_default = NULL; +static gboolean use_ellipsis_default = TRUE; + +static void +e_cell_text_class_init (ECellTextClass *class) +{ + ECellClass *ecc = E_CELL_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + const gchar *ellipsis_env; + + object_class->finalize = ect_finalize; + + ecc->new_view = ect_new_view; + ecc->kill_view = ect_kill_view; + ecc->realize = ect_realize; + ecc->unrealize = ect_unrealize; + ecc->draw = ect_draw; + ecc->event = ect_event; + ecc->height = ect_height; + ecc->enter_edit = ect_enter_edit; + ecc->leave_edit = ect_leave_edit; + ecc->save_state = ect_save_state; + ecc->load_state = ect_load_state; + ecc->free_state = ect_free_state; + ecc->print = ect_print; + ecc->print_height = ect_print_height; + ecc->max_width = ect_max_width; + ecc->max_width_by_row = ect_max_width_by_row; + ecc->get_bg_color = ect_get_bg_color; + + class->get_text = ect_real_get_text; + class->free_text = ect_real_free_text; + class->set_value = ect_real_set_value; + + object_class->get_property = ect_get_property; + object_class->set_property = ect_set_property; + + signals[TEXT_INSERTED] = g_signal_new ( + "text_inserted", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (ECellTextClass, text_inserted), + NULL, NULL, + e_marshal_VOID__POINTER_INT_INT_INT_INT, + G_TYPE_NONE, 5, + G_TYPE_POINTER, + G_TYPE_INT, + G_TYPE_INT, + G_TYPE_INT, + G_TYPE_INT); + + signals[TEXT_DELETED] = g_signal_new ( + "text_deleted", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (ECellTextClass, text_deleted), + NULL, NULL, + e_marshal_VOID__POINTER_INT_INT_INT_INT, + G_TYPE_NONE, 5, + G_TYPE_POINTER, + G_TYPE_INT, + G_TYPE_INT, + G_TYPE_INT, + G_TYPE_INT); + + g_object_class_install_property ( + object_class, + PROP_STRIKEOUT_COLUMN, + g_param_spec_int ( + "strikeout_column", + "Strikeout Column", + NULL, + -1, G_MAXINT, -1, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_UNDERLINE_COLUMN, + g_param_spec_int ( + "underline_column", + "Underline Column", + NULL, + -1, G_MAXINT, -1, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_BOLD_COLUMN, + g_param_spec_int ( + "bold_column", + "Bold Column", + NULL, + -1, G_MAXINT, -1, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_COLOR_COLUMN, + g_param_spec_int ( + "color_column", + "Color Column", + NULL, + -1, G_MAXINT, -1, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_EDITABLE, + g_param_spec_boolean ( + "editable", + "Editable", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_BG_COLOR_COLUMN, + g_param_spec_int ( + "bg_color_column", + "BG Color Column", + NULL, + -1, G_MAXINT, -1, + G_PARAM_READWRITE)); + + if (!clipboard_atom) + clipboard_atom = gdk_atom_intern ("CLIPBOARD", FALSE); + + ellipsis_env = g_getenv ("GAL_ELLIPSIS"); + if (ellipsis_env) { + if (*ellipsis_env) { + ellipsis_default = g_strdup (ellipsis_env); + } else { + use_ellipsis_default = FALSE; + } + } +} + +/* IM Context Callbacks */ + +static void +e_cell_text_get_cursor_locations (ECellTextView *tv, + GdkRectangle *strong_pos, + GdkRectangle *weak_pos) +{ + GdkRectangle area; + CellEdit *edit = tv->edit; + ECellView *cell_view = (ECellView *) tv; + ETableItem *item = E_TABLE_ITEM ((cell_view)->e_table_item_view); + GnomeCanvasItem *parent_item = GNOME_CANVAS_ITEM (item)->parent; + PangoRectangle pango_strong_pos; + PangoRectangle pango_weak_pos; + gint x, y, col, row; + gdouble x1,y1; + gint cx, cy; + gint index; + + row = edit->row; + col = edit->view_col; + + e_table_item_get_cell_geometry ( + item, &row, &col, &x, &y, NULL, &area.height); + + gnome_canvas_item_get_bounds (GNOME_CANVAS_ITEM (parent_item), &x1, &y1, NULL, NULL); + + gnome_canvas_get_scroll_offsets (GNOME_CANVAS (GNOME_CANVAS_ITEM (parent_item)->canvas), &cx, &cy); + + index = edit->selection_end + edit->preedit_pos; + + pango_layout_get_cursor_pos ( + edit->layout, + index, + strong_pos ? &pango_strong_pos : NULL, + weak_pos ? &pango_weak_pos : NULL); + + if (strong_pos) { + strong_pos->x = x + x1 - cx - edit->xofs_edit + pango_strong_pos.x / PANGO_SCALE; + strong_pos->y = y + y1 - cy - edit->yofs_edit + pango_strong_pos.y / PANGO_SCALE; + strong_pos->width = 0; + strong_pos->height = pango_strong_pos.height / PANGO_SCALE; + } + + if (weak_pos) { + weak_pos->x = x + x1 - cx - edit->xofs_edit + pango_weak_pos.x / PANGO_SCALE; + weak_pos->y = y + y1 - cy - edit->yofs_edit + pango_weak_pos.y / PANGO_SCALE; + weak_pos->width = 0; + weak_pos->height = pango_weak_pos.height / PANGO_SCALE; + } +} + +static void +update_im_cursor_location (ECellTextView *tv) +{ + CellEdit *edit = tv->edit; + GdkRectangle area; + + e_cell_text_get_cursor_locations (tv, &area, NULL); + + gtk_im_context_set_cursor_location (edit->im_context, &area); +} + +static void +e_cell_text_preedit_changed_cb (GtkIMContext *context, + ECellTextView *tv) +{ + gchar *preedit_string; + gint cursor_pos; + CellEdit *edit = tv->edit; + gtk_im_context_get_preedit_string ( + edit->im_context, &preedit_string, + NULL, &cursor_pos); + + edit->preedit_length = strlen (preedit_string); + cursor_pos = CLAMP (cursor_pos, 0, g_utf8_strlen (preedit_string, -1)); + edit->preedit_pos = g_utf8_offset_to_pointer (preedit_string, cursor_pos) - preedit_string; + g_free (preedit_string); + + ect_queue_redraw (tv, edit->view_col, edit->row); +} + +static void +e_cell_text_commit_cb (GtkIMContext *context, + const gchar *str, + ECellTextView *tv) +{ + CellEdit *edit = tv->edit; + ETextEventProcessorCommand command = { 0 }; + + if (g_utf8_validate (str, strlen (str), NULL)) { + command.action = E_TEP_INSERT; + command.position = E_TEP_SELECTION; + command.string = (gchar *) str; + command.value = strlen (str); + e_cell_text_view_command (edit->tep, &command, edit); + } + +} + +static gboolean +e_cell_text_retrieve_surrounding_cb (GtkIMContext *context, + ECellTextView *tv) +{ + CellEdit *edit = tv->edit; + + gtk_im_context_set_surrounding ( + context, + edit->text, + strlen (edit->text), + MIN (edit->selection_start, edit->selection_end)); + + return TRUE; +} + +static gboolean +e_cell_text_delete_surrounding_cb (GtkIMContext *context, + gint offset, + gint n_chars, + ECellTextView *tv) +{ + gint begin_pos, end_pos; + glong text_len; + CellEdit *edit = tv->edit; + + text_len = g_utf8_strlen (edit->text, -1); + begin_pos = g_utf8_pointer_to_offset ( + edit->text, + edit->text + MIN (edit->selection_start, edit->selection_end)); + begin_pos += offset; + end_pos = begin_pos + n_chars; + if (begin_pos < 0 || text_len < begin_pos) + return FALSE; + if (end_pos > text_len) + end_pos = text_len; + edit->selection_start = g_utf8_offset_to_pointer (edit->text, begin_pos) + - edit->text; + edit->selection_end = g_utf8_offset_to_pointer (edit->text, end_pos) + - edit->text; + + _delete_selection (tv); + + return TRUE; +} + +static void +e_cell_text_init (ECellText *ect) +{ + ect->ellipsis = g_strdup (ellipsis_default); + ect->use_ellipsis = use_ellipsis_default; + ect->strikeout_column = -1; + ect->underline_column = -1; + ect->bold_column = -1; + ect->color_column = -1; + ect->bg_color_column = -1; + ect->editable = TRUE; +} + +/** + * e_cell_text_new: + * @fontname: this param is no longer used, but left here for api stability + * @justify: Justification of the string in the cell. + * + * Creates a new ECell renderer that can be used to render strings that + * that come from the model. The value returned from the model is + * interpreted as being a gchar *. + * + * The ECellText object support a large set of properties that can be + * configured through the Gtk argument system and allows the user to have + * a finer control of the way the string is displayed. The arguments supported + * allow the control of strikeout, underline, bold, and color. + * + * The arguments "strikeout_column", "underline_column", "bold_column" + * and "color_column" set and return an integer that points to a + * column in the model that controls these settings. So controlling + * the way things are rendered is achieved by having special columns + * in the model that will be used to flag whether the text should be + * rendered with strikeout, or bolded. In the case of the + * "color_column" argument, the column in the model is expected to + * have a string that can be parsed by gdk_color_parse(). + * + * Returns: an ECell object that can be used to render strings. + */ +ECell * +e_cell_text_new (const gchar *fontname, + GtkJustification justify) +{ + ECellText *ect = g_object_new (E_TYPE_CELL_TEXT, NULL); + + e_cell_text_construct (ect, fontname, justify); + + return (ECell *) ect; +} + +/** + * e_cell_text_construct: + * @cell: The cell to construct + * @fontname: this param is no longer used, but left here for api stability + * @justify: Justification of the string in the cell + * + * constructs the ECellText. To be used by subclasses and language + * bindings. + * + * Returns: The ECellText. + */ +ECell * +e_cell_text_construct (ECellText *cell, + const gchar *fontname, + GtkJustification justify) +{ + if (!cell) + return E_CELL (NULL); + if (fontname) + cell->font_name = g_strdup (fontname); + cell->justify = justify; + return E_CELL (cell); +} + +gchar * +e_cell_text_get_text (ECellText *cell, + ETableModel *model, + gint col, + gint row) +{ + ECellTextClass *class; + + g_return_val_if_fail (E_IS_CELL_TEXT (cell), NULL); + + class = E_CELL_TEXT_GET_CLASS (cell); + if (class->get_text == NULL) + return NULL; + + return class->get_text (cell, model, col, row); +} + +void +e_cell_text_free_text (ECellText *cell, + gchar *text) +{ + ECellTextClass *class; + + g_return_if_fail (E_IS_CELL_TEXT (cell)); + + class = E_CELL_TEXT_GET_CLASS (cell); + if (class->free_text == NULL) + return; + + class->free_text (cell, text); +} + +void +e_cell_text_set_value (ECellText *cell, + ETableModel *model, + gint col, + gint row, + const gchar *text) +{ + ECellTextClass *class; + + g_return_if_fail (E_IS_CELL_TEXT (cell)); + + class = E_CELL_TEXT_GET_CLASS (cell); + if (class->set_value == NULL) + return; + + class->set_value (cell, model, col, row, text); +} + +/* fixme: Handle Font attributes */ +/* position is in BYTES */ + +static gint +get_position_from_xy (CellEdit *edit, + gint x, + gint y) +{ + gint index; + gint trailing; + const gchar *text; + + PangoLayout *layout = generate_layout (edit->text_view, edit->model_col, edit->view_col, edit->row, edit->cell_width); + ECellTextView *text_view = edit->text_view; + ECellText *ect = (ECellText *) ((ECellView *) text_view)->ecell; + + x -= (ect->x + text_view->xofs - edit->xofs_edit); + y -= (ect->y + text_view->yofs - edit->yofs_edit); + + pango_layout_xy_to_index (layout, x * PANGO_SCALE, y * PANGO_SCALE, &index, &trailing); + + text = pango_layout_get_text (layout); + + return g_utf8_offset_to_pointer (text + index, trailing) - text; +} + +#define SCROLL_WAIT_TIME 30000 + +static gboolean +_blink_scroll_timeout (gpointer data) +{ + ECellTextView *text_view = (ECellTextView *) data; + ECellText *ect = E_CELL_TEXT (((ECellView *) text_view)->ecell); + CellEdit *edit = text_view->edit; + + gulong current_time; + gboolean scroll = FALSE; + gboolean redraw = FALSE; + gint width, height; + + g_timer_elapsed (edit->timer, ¤t_time); + + if (edit->scroll_start + SCROLL_WAIT_TIME > 1000000) { + if (current_time > edit->scroll_start - (1000000 - SCROLL_WAIT_TIME) && + current_time < edit->scroll_start) + scroll = TRUE; + } else { + if (current_time > edit->scroll_start + SCROLL_WAIT_TIME || + current_time < edit->scroll_start) + scroll = TRUE; + } + + pango_layout_get_pixel_size (edit->layout, &width, &height); + + if (scroll && edit->button_down) { + /* FIXME: Copy this for y. */ + if (edit->lastx - ect->x > edit->cell_width) { + if (edit->xofs_edit < width - edit->cell_width) { + edit->xofs_edit += 4; + if (edit->xofs_edit > width - edit->cell_width + 1) + edit->xofs_edit = width - edit->cell_width + 1; + redraw = TRUE; + } + } + if (edit->lastx - ect->x < 0 && + edit->xofs_edit > 0) { + edit->xofs_edit -= 4; + if (edit->xofs_edit < 0) + edit->xofs_edit = 0; + redraw = TRUE; + } + if (redraw) { + ETextEventProcessorEvent e_tep_event; + e_tep_event.type = GDK_MOTION_NOTIFY; + e_tep_event.motion.state = edit->last_state; + e_tep_event.motion.time = 0; + e_tep_event.motion.position = get_position_from_xy (edit, edit->lastx, edit->lasty); + _get_tep (edit); + e_text_event_processor_handle_event ( + edit->tep, + &e_tep_event); + edit->scroll_start = current_time; + } + } + + if (!((current_time / 500000) % 2)) { + if (!edit->show_cursor) + redraw = TRUE; + edit->show_cursor = TRUE; + } else { + if (edit->show_cursor) + redraw = TRUE; + edit->show_cursor = FALSE; + } + if (redraw) { + ect_queue_redraw (text_view, edit->view_col, edit->row); + } + return TRUE; +} + +static gint +next_word (CellEdit *edit, + gint start) +{ + gchar *p; + gint length; + + length = strlen (edit->text); + if (start >= length) + return length; + + p = g_utf8_next_char (edit->text + start); + + while (*p && g_unichar_validate (g_utf8_get_char (p))) { + gunichar unival = g_utf8_get_char (p); + if (g_unichar_isspace (unival)) + return p - edit->text; + p = g_utf8_next_char (p); + } + + return p - edit->text; +} + +static gint +_get_position (ECellTextView *text_view, + ETextEventProcessorCommand *command) +{ + gint length; + CellEdit *edit = text_view->edit; + gchar *p; + gint unival; + gint index; + gint trailing; + + switch (command->position) { + + case E_TEP_VALUE: + return command->value; + + case E_TEP_SELECTION: + return edit->selection_end; + + case E_TEP_START_OF_BUFFER: + return 0; + + /* fixme: this probably confuses TEP */ + + case E_TEP_END_OF_BUFFER: + return strlen (edit->text); + + case E_TEP_START_OF_LINE: + + if (edit->selection_end < 1) return 0; + + p = g_utf8_find_prev_char (edit->text, edit->text + edit->selection_end); + + if (p == edit->text) return 0; + + p = g_utf8_find_prev_char (edit->text, p); + + while (p && p > edit->text) { + if (*p == '\n') return p - edit->text + 1; + p = g_utf8_find_prev_char (edit->text, p); + } + + return 0; + + case E_TEP_END_OF_LINE: + + length = strlen (edit->text); + if (edit->selection_end >= length) return length; + + p = g_utf8_next_char (edit->text + edit->selection_end); + + while (*p && g_unichar_validate (g_utf8_get_char (p))) { + if (*p == '\n') return p - edit->text; + p = g_utf8_next_char (p); + } + + return p - edit->text; + + case E_TEP_FORWARD_CHARACTER: + + length = strlen (edit->text); + if (edit->selection_end >= length) return length; + + p = g_utf8_next_char (edit->text + edit->selection_end); + + return p - edit->text; + + case E_TEP_BACKWARD_CHARACTER: + + if (edit->selection_end < 1) return 0; + + p = g_utf8_find_prev_char (edit->text, edit->text + edit->selection_end); + + if (p == NULL) return 0; + + return p - edit->text; + + case E_TEP_FORWARD_WORD: + return next_word (edit, edit->selection_end); + + case E_TEP_BACKWARD_WORD: + + if (edit->selection_end < 1) return 0; + + p = g_utf8_find_prev_char (edit->text, edit->text + edit->selection_end); + + if (p == edit->text) return 0; + + p = g_utf8_find_prev_char (edit->text, p); + + while (p && p > edit->text && g_unichar_validate (g_utf8_get_char (p))) { + unival = g_utf8_get_char (p); + if (g_unichar_isspace (unival)) { + return (g_utf8_next_char (p) - edit->text); + } + p = g_utf8_find_prev_char (edit->text, p); + } + + return 0; + + case E_TEP_FORWARD_LINE: + pango_layout_move_cursor_visually ( + edit->layout, + TRUE, + edit->selection_end, + 0, + TRUE, + &index, + &trailing); + index = g_utf8_offset_to_pointer (edit->text + index, trailing) - edit->text; + if (index < 0) + return 0; + length = strlen (edit->text); + if (index >= length) + return length; + return index; + case E_TEP_BACKWARD_LINE: + pango_layout_move_cursor_visually ( + edit->layout, + TRUE, + edit->selection_end, + 0, + TRUE, + &index, + &trailing); + + index = g_utf8_offset_to_pointer (edit->text + index, trailing) - edit->text; + if (index < 0) + return 0; + length = strlen (edit->text); + if (index >= length) + return length; + return index; + case E_TEP_FORWARD_PARAGRAPH: + case E_TEP_BACKWARD_PARAGRAPH: + + case E_TEP_FORWARD_PAGE: + case E_TEP_BACKWARD_PAGE: + return edit->selection_end; + default: + return edit->selection_end; + } + + g_return_val_if_reached (0); + + return 0; /* Kill warning */ +} + +static void +_delete_selection (ECellTextView *text_view) +{ + CellEdit *edit = text_view->edit; + gint length; + gchar *sp, *ep; + + if (edit->selection_end == edit->selection_start) return; + + if (edit->selection_end < edit->selection_start) { + edit->selection_end ^= edit->selection_start; + edit->selection_start ^= edit->selection_end; + edit->selection_end ^= edit->selection_start; + } + + sp = edit->text + edit->selection_start; + ep = edit->text + edit->selection_end; + length = strlen (ep) + 1; + + memmove (sp, ep, length); + + edit->selection_end = edit->selection_start; + + g_signal_emit (VIEW_TO_CELL (text_view), signals[TEXT_DELETED], 0, text_view, edit->selection_start, ep - sp, edit->row, edit->model_col); +} + +/* fixme: */ +/* NB! We expect value to be length IN BYTES */ + +static void +_insert (ECellTextView *text_view, + const gchar *string, + gint value) +{ + CellEdit *edit = text_view->edit; + gchar *temp; + + if (value <= 0) return; + + edit->selection_start = MIN (strlen (edit->text), edit->selection_start); + + temp = g_new (gchar, strlen (edit->text) + value + 1); + + strncpy (temp, edit->text, edit->selection_start); + strncpy (temp + edit->selection_start, string, value); + strcpy (temp + edit->selection_start + value, edit->text + edit->selection_end); + + g_free (edit->text); + + edit->text = temp; + + edit->selection_start += value; + edit->selection_end = edit->selection_start; + + g_signal_emit (VIEW_TO_CELL (text_view), signals[TEXT_INSERTED], 0, text_view, edit->selection_end - value, value, edit->row, edit->model_col); +} + +static void +capitalize (CellEdit *edit, + gint start, + gint end, + ETextEventProcessorCaps type) +{ + ECellTextView *text_view = edit->text_view; + + gboolean first = TRUE; + gint character_length = g_utf8_strlen (edit->text + start, start - end); + const gchar *p = edit->text + start; + const gchar *text_end = edit->text + end; + gchar *new_text = g_new0 (char, character_length * 6 + 1); + gchar *output = new_text; + + while (p && *p && p < text_end && g_unichar_validate (g_utf8_get_char (p))) { + gunichar unival = g_utf8_get_char (p); + gunichar newval = unival; + + switch (type) { + case E_TEP_CAPS_UPPER: + newval = g_unichar_toupper (unival); + break; + case E_TEP_CAPS_LOWER: + newval = g_unichar_tolower (unival); + break; + case E_TEP_CAPS_TITLE: + if (g_unichar_isalpha (unival)) { + if (first) + newval = g_unichar_totitle (unival); + else + newval = g_unichar_tolower (unival); + first = FALSE; + } else { + first = TRUE; + } + break; + } + g_unichar_to_utf8 (newval, output); + output = g_utf8_next_char (output); + + p = g_utf8_next_char (p); + } + *output = 0; + + edit->selection_end = end; + edit->selection_start = start; + _delete_selection (text_view); + + _insert (text_view, new_text, output - new_text); + + g_free (new_text); +} + +static void +e_cell_text_view_command (ETextEventProcessor *tep, + ETextEventProcessorCommand *command, + gpointer data) +{ + CellEdit *edit = (CellEdit *) data; + ECellTextView *text_view = edit->text_view; + ECellText *ect = E_CELL_TEXT (text_view->cell_view.ecell); + + gboolean change = FALSE; + gboolean redraw = FALSE; + + gint sel_start, sel_end; + + /* If the EText isn't editable, then ignore any commands that would + * modify the text. */ + if (!ect->editable && (command->action == E_TEP_DELETE + || command->action == E_TEP_INSERT + || command->action == E_TEP_PASTE + || command->action == E_TEP_GET_SELECTION)) + return; + + switch (command->action) { + case E_TEP_MOVE: + edit->selection_start = _get_position (text_view, command); + edit->selection_end = edit->selection_start; + if (edit->timer) { + g_timer_reset (edit->timer); + } + redraw = TRUE; + break; + case E_TEP_SELECT: + edit->selection_end = _get_position (text_view, command); + sel_start = MIN (edit->selection_start, edit->selection_end); + sel_end = MAX (edit->selection_start, edit->selection_end); + if (sel_start != sel_end) { + e_cell_text_view_supply_selection ( + edit, command->time, GDK_SELECTION_PRIMARY, + edit->text + sel_start, + sel_end - sel_start); + } else if (edit->timer) { + g_timer_reset (edit->timer); + } + redraw = TRUE; + break; + case E_TEP_DELETE: + if (edit->selection_end == edit->selection_start) { + edit->selection_end = _get_position (text_view, command); + } + _delete_selection (text_view); + if (edit->timer) { + g_timer_reset (edit->timer); + } + redraw = TRUE; + change = TRUE; + break; + + case E_TEP_INSERT: + if (!edit->preedit_length && edit->selection_end != edit->selection_start) { + _delete_selection (text_view); + } + _insert (text_view, command->string, command->value); + if (edit->timer) { + g_timer_reset (edit->timer); + } + redraw = TRUE; + change = TRUE; + break; + case E_TEP_COPY: + sel_start = MIN (edit->selection_start, edit->selection_end); + sel_end = MAX (edit->selection_start, edit->selection_end); + if (sel_start != sel_end) { + e_cell_text_view_supply_selection ( + edit, command->time, clipboard_atom, + edit->text + sel_start, + sel_end - sel_start); + } + if (edit->timer) { + g_timer_reset (edit->timer); + } + break; + case E_TEP_PASTE: + e_cell_text_view_get_selection (edit, clipboard_atom, command->time); + if (edit->timer) { + g_timer_reset (edit->timer); + } + redraw = TRUE; + change = TRUE; + break; + case E_TEP_GET_SELECTION: + e_cell_text_view_get_selection (edit, GDK_SELECTION_PRIMARY, command->time); + break; + case E_TEP_ACTIVATE: + e_table_item_leave_edit_ (text_view->cell_view.e_table_item_view); + break; + case E_TEP_SET_SELECT_BY_WORD: + edit->select_by_word = command->value; + break; + case E_TEP_GRAB: + edit->actions = E_CELL_GRAB; + break; + case E_TEP_UNGRAB: + edit->actions = E_CELL_UNGRAB; + break; + case E_TEP_CAPS: + if (edit->selection_start == edit->selection_end) { + capitalize (edit, edit->selection_start, next_word (edit, edit->selection_start), command->value); + } else { + gint selection_start = MIN (edit->selection_start, edit->selection_end); + gint selection_end = edit->selection_start + edit->selection_end - selection_start; /* Slightly faster than MAX */ + capitalize (edit, selection_start, selection_end, command->value); + } + if (edit->timer) { + g_timer_reset (edit->timer); + } + redraw = TRUE; + change = TRUE; + break; + case E_TEP_NOP: + break; + } + + if (change) { + if (edit->layout) + g_object_unref (edit->layout); + edit->layout = build_layout (text_view, edit->row, edit->text, edit->cell_width); + } + + if (!edit->button_down) { + PangoRectangle strong_pos, weak_pos; + pango_layout_get_cursor_pos (edit->layout, edit->selection_end, &strong_pos, &weak_pos); + if (strong_pos.x != weak_pos.x || + strong_pos.y != weak_pos.y || + strong_pos.width != weak_pos.width || + strong_pos.height != weak_pos.height) { + if (show_pango_rectangle (edit, weak_pos)) + redraw = TRUE; + } + if (show_pango_rectangle (edit, strong_pos)) { + redraw = TRUE; + } + } + + if (redraw) { + ect_queue_redraw (text_view, edit->view_col, edit->row); + } +} + +static void +e_cell_text_view_supply_selection (CellEdit *edit, + guint time, + GdkAtom selection, + gchar *data, + gint length) +{ +#if DO_SELECTION + GtkClipboard *clipboard; + + clipboard = gtk_widget_get_clipboard (GTK_WIDGET (edit->text_view->canvas), selection); + + if (selection == GDK_SELECTION_PRIMARY) { + edit->has_selection = TRUE; + } + + gtk_clipboard_set_text (clipboard, data, length); +#endif +} + +#ifdef DO_SELECTION +static void +paste_received (GtkClipboard *clipboard, + const gchar *text, + gpointer data) +{ + CellEdit *edit; + + g_return_if_fail (data); + + edit = (CellEdit *) data; + + if (text && g_utf8_validate (text, strlen (text), NULL)) { + ETextEventProcessorCommand command = { 0 }; + command.action = E_TEP_INSERT; + command.position = E_TEP_SELECTION; + command.string = (gchar *) text; + command.value = strlen (text); + command.time = GDK_CURRENT_TIME; + e_cell_text_view_command (edit->tep, &command, edit); + } +} +#endif + +static void +e_cell_text_view_get_selection (CellEdit *edit, + GdkAtom selection, + guint32 time) +{ +#if DO_SELECTION + gtk_clipboard_request_text ( + gtk_widget_get_clipboard (GTK_WIDGET (edit->text_view->canvas), + selection), + paste_received, edit); +#endif +} + +static void +_get_tep (CellEdit *edit) +{ + if (!edit->tep) { + edit->tep = e_text_event_processor_emacs_like_new (); + g_signal_connect ( + edit->tep, "command", + G_CALLBACK (e_cell_text_view_command), edit); + } +} + +/** + * e_cell_text_set_selection: + * @cell_view: the given cell view + * @col: column of the given cell in the view + * @row: row of the given cell in the view + * @start: start offset of the selection + * @end: end offset of the selection + * + * Sets the selection of given text cell. + * If the current editing cell is not the given cell, this function + * will return FALSE; + * + * If success, the [start, end) part of the text will be selected. + * + * This API is most likely to be used by a11y implementations. + * + * Returns: whether the action is successful. + */ +gboolean +e_cell_text_set_selection (ECellView *cell_view, + gint col, + gint row, + gint start, + gint end) +{ + ECellTextView *ectv; + CellEdit *edit; + ETextEventProcessorCommand command1 = { 0 }, command2 = { 0 }; + + g_return_val_if_fail (cell_view != NULL, FALSE); + + ectv = (ECellTextView *) cell_view; + edit = ectv->edit; + if (!edit) + return FALSE; + + if (edit->view_col != col || edit->row != row) + return FALSE; + + command1.action = E_TEP_MOVE; + command1.position = E_TEP_VALUE; + command1.value = start; + e_cell_text_view_command (edit->tep, &command1, edit); + + command2.action = E_TEP_SELECT; + command2.position = E_TEP_VALUE; + command2.value = end; + e_cell_text_view_command (edit->tep, &command2, edit); + + return TRUE; +} + +/** + * e_cell_text_get_selection: + * @cell_view: the given cell view + * @col: column of the given cell in the view + * @row: row of the given cell in the view + * @start: a pointer to an gint value indicates the start offset of the selection + * @end: a pointer to an gint value indicates the end offset of the selection + * + * Gets the selection of given text cell. + * If the current editing cell is not the given cell, this function + * will return FALSE; + * + * This API is most likely to be used by a11y implementations. + * + * Returns: whether the action is successful. + */ +gboolean +e_cell_text_get_selection (ECellView *cell_view, + gint col, + gint row, + gint *start, + gint *end) +{ + ECellTextView *ectv; + CellEdit *edit; + + g_return_val_if_fail (cell_view != NULL, FALSE); + + ectv = (ECellTextView *) cell_view; + edit = ectv->edit; + if (!edit) + return FALSE; + + if (edit->view_col != col || edit->row != row) + return FALSE; + + if (start) + *start = edit->selection_start; + if (end) + *end = edit->selection_end; + return TRUE; +} + +/** + * e_cell_text_copy_clipboard: + * @cell_view: the given cell view + * @col: column of the given cell in the view + * @row: row of the given cell in the view + * + * Copys the selected text to clipboard. + * + * This API is most likely to be used by a11y implementations. + */ +void +e_cell_text_copy_clipboard (ECellView *cell_view, + gint col, + gint row) +{ + ECellTextView *ectv; + CellEdit *edit; + ETextEventProcessorCommand command = { 0 }; + + g_return_if_fail (cell_view != NULL); + + ectv = (ECellTextView *) cell_view; + edit = ectv->edit; + if (!edit) + return; + + if (edit->view_col != col || edit->row != row) + return; + + command.action = E_TEP_COPY; + command.time = GDK_CURRENT_TIME; + e_cell_text_view_command (edit->tep, &command, edit); +} + +/** + * e_cell_text_paste_clipboard: + * @cell_view: the given cell view + * @col: column of the given cell in the view + * @row: row of the given cell in the view + * + * Pastes the text from the clipboardt. + * + * This API is most likely to be used by a11y implementations. + */ +void +e_cell_text_paste_clipboard (ECellView *cell_view, + gint col, + gint row) +{ + ECellTextView *ectv; + CellEdit *edit; + ETextEventProcessorCommand command = { 0 }; + + g_return_if_fail (cell_view != NULL); + + ectv = (ECellTextView *) cell_view; + edit = ectv->edit; + if (!edit) + return; + + if (edit->view_col != col || edit->row != row) + return; + + command.action = E_TEP_PASTE; + command.time = GDK_CURRENT_TIME; + e_cell_text_view_command (edit->tep, &command, edit); +} + +/** + * e_cell_text_delete_selection: + * @cell_view: the given cell view + * @col: column of the given cell in the view + * @row: row of the given cell in the view + * + * Deletes the selected text of the cell. + * + * This API is most likely to be used by a11y implementations. + */ +void +e_cell_text_delete_selection (ECellView *cell_view, + gint col, + gint row) +{ + ECellTextView *ectv; + CellEdit *edit; + ETextEventProcessorCommand command = { 0 }; + + g_return_if_fail (cell_view != NULL); + + ectv = (ECellTextView *) cell_view; + edit = ectv->edit; + if (!edit) + return; + + if (edit->view_col != col || edit->row != row) + return; + + command.action = E_TEP_DELETE; + command.position = E_TEP_SELECTION; + e_cell_text_view_command (edit->tep, &command, edit); +} + +/** + * e_cell_text_get_text_by_view: + * @cell_view: the given cell view + * @col: column of the given cell in the model + * @row: row of the given cell in the model + * + * Get the cell's text directly from CellEdit, + * during editting this cell, the cell's text value maybe inconsistant + * with the text got from table_model. + * The caller should free the text after using it. + * + * This API is most likely to be used by a11y implementations. + */ +gchar * +e_cell_text_get_text_by_view (ECellView *cell_view, + gint col, + gint row) +{ + ECellTextView *ectv; + CellEdit *edit; + gchar *ret, *model_text; + + g_return_val_if_fail (cell_view != NULL, NULL); + + ectv = (ECellTextView *) cell_view; + edit = ectv->edit; + + if (edit && ectv->edit->row == row && ectv->edit->model_col == col) { /* being editted now */ + ret = g_strdup (edit->text); + } else{ + model_text = e_cell_text_get_text ( + E_CELL_TEXT (cell_view->ecell), + cell_view->e_table_model, col, row); + ret = g_strdup (model_text); + e_cell_text_free_text (E_CELL_TEXT (cell_view->ecell), model_text); + } + + return ret; + +} diff --git a/e-util/e-cell-text.h b/e-util/e-cell-text.h new file mode 100644 index 0000000000..740b87fec7 --- /dev/null +++ b/e-util/e-cell-text.h @@ -0,0 +1,195 @@ +/* + * Text cell renderer. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * A lot of code taken from: + * + * Text item type for GnomeCanvas widget + * + * GnomeCanvas is basically a port of the Tk toolkit's most excellent + * canvas widget. Tk is copyrighted by the Regents of the University + * of California, Sun Microsystems, and other parties. + * + * Copyright (C) 1998 The Free Software Foundation + * + * Author: Federico Mena <federico@nuclecu.unam.mx> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CELL_TEXT_H +#define E_CELL_TEXT_H + +#include <gtk/gtk.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-cell.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_TEXT \ + (e_cell_text_get_type ()) +#define E_CELL_TEXT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_TEXT, ECellText)) +#define E_CELL_TEXT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_TEXT, ECellTextClass)) +#define E_IS_CELL_TEXT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_TEXT)) +#define E_IS_CELL_TEXT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_TEXT)) +#define E_CELL_TEXT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_TEXT, ECellTextClass)) + +G_BEGIN_DECLS + +typedef struct _ECellText ECellText; +typedef struct _ECellTextClass ECellTextClass; + +struct _ECellText { + ECell parent; + + GtkJustification justify; + gchar *font_name; + + gdouble x, y; /* Position at anchor */ + + gulong pixel; /* Fill color */ + + /* Clip handling */ + gchar *ellipsis; /* The ellipsis characters. NULL = "...". */ + + guint use_ellipsis : 1; /* Whether to use the ellipsis. */ + guint editable : 1; /* Whether the text can be edited. */ + + gint strikeout_column; + gint underline_column; + gint bold_column; + + /* This column in the ETable should return a string specifying a color, + * either a color name like "red" or a color spec like "rgb:F/0/0". + * See the XParseColor man page for the formats available. */ + gint color_column; + gint bg_color_column; +}; + +struct _ECellTextClass { + ECellClass parent_class; + + /* Methods */ + gchar * (*get_text) (ECellText *cell, + ETableModel *model, + gint col, + gint row); + void (*free_text) (ECellText *cell, + gchar *text); + void (*set_value) (ECellText *cell, + ETableModel *model, + gint col, + gint row, + const gchar *text); + + /* Signals */ + void (*text_inserted) (ECellText *cell, + ECellView *cell_view, + gint pos, + gint len, + gint row, + gint model_col); + void (*text_deleted) (ECellText *cell, + ECellView *cell_view, + gint pos, + gint len, + gint row, + gint model_col); +}; + +GType e_cell_text_get_type (void) G_GNUC_CONST; +ECell * e_cell_text_new (const gchar *fontname, + GtkJustification justify); +ECell * e_cell_text_construct (ECellText *cell, + const gchar *fontname, + GtkJustification justify); + +/* Gets the value from the model and converts it into a string. In ECellText + * itself, the value is assumed to be a gchar * and so needs no conversion. + * In subclasses the ETableModel value may be a more complicated datatype. */ +gchar * e_cell_text_get_text (ECellText *cell, + ETableModel *model, + gint col, + gint row); + +/* Frees the value returned by e_cell_text_get_text(). */ +void e_cell_text_free_text (ECellText *cell, + gchar *text); + +/* Sets the ETableModel value, based on the given string. */ +void e_cell_text_set_value (ECellText *cell, + ETableModel *model, + gint col, + gint row, + const gchar *text); + +/* Sets the selection of given text cell */ +gboolean e_cell_text_set_selection (ECellView *cell_view, + gint col, + gint row, + gint start, + gint end); + +/* Gets the selection of given text cell */ +gboolean e_cell_text_get_selection (ECellView *cell_view, + gint col, + gint row, + gint *start, + gint *end); + +/* Copys the selected text to the clipboard */ +void e_cell_text_copy_clipboard (ECellView *cell_view, + gint col, + gint row); + +/* Pastes the text from the clipboard */ +void e_cell_text_paste_clipboard (ECellView *cell_view, + gint col, + gint row); + +/* Deletes selected text */ +void e_cell_text_delete_selection (ECellView *cell_view, + gint col, + gint row); + +/* get text directly from view, both col and row are model format */ +gchar * e_cell_text_get_text_by_view (ECellView *cell_view, + gint col, + gint row); + +G_END_DECLS + +#endif /* E_CELL_TEXT_H */ + diff --git a/e-util/e-cell-toggle.c b/e-util/e-cell-toggle.c new file mode 100644 index 0000000000..2f2bcd359c --- /dev/null +++ b/e-util/e-cell-toggle.c @@ -0,0 +1,469 @@ +/* + * e-cell-toggle.c - Multi-state image toggle cell object. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include "art/empty.xpm" + +#include "gal-a11y-e-cell-toggle.h" +#include "gal-a11y-e-cell-registry.h" + +#include "e-cell-toggle.h" +#include "e-table-item.h" + +#define E_CELL_TOGGLE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CELL_TOGGLE, ECellTogglePrivate)) + +struct _ECellTogglePrivate { + gchar **icon_names; + guint n_icon_names; + + GdkPixbuf *empty; + GPtrArray *pixbufs; + gint height; +}; + +G_DEFINE_TYPE (ECellToggle, e_cell_toggle, E_TYPE_CELL) + +typedef struct { + ECellView cell_view; + GnomeCanvas *canvas; +} ECellToggleView; + +static void +cell_toggle_load_icons (ECellToggle *cell_toggle) +{ + GtkIconTheme *icon_theme; + gint width, height; + gint max_height = 0; + guint ii; + GError *error = NULL; + + icon_theme = gtk_icon_theme_get_default (); + gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, &height); + + g_ptr_array_set_size (cell_toggle->priv->pixbufs, 0); + + for (ii = 0; ii < cell_toggle->priv->n_icon_names; ii++) { + const gchar *icon_name = cell_toggle->priv->icon_names[ii]; + GdkPixbuf *pixbuf = NULL; + + if (icon_name != NULL) + pixbuf = gtk_icon_theme_load_icon ( + icon_theme, icon_name, height, 0, &error); + + if (error != NULL) { + g_warning ("%s", error->message); + g_clear_error (&error); + } + + if (pixbuf == NULL) + pixbuf = g_object_ref (cell_toggle->priv->empty); + + g_ptr_array_add (cell_toggle->priv->pixbufs, pixbuf); + max_height = MAX (max_height, gdk_pixbuf_get_height (pixbuf)); + } + + cell_toggle->priv->height = max_height; +} + +static void +cell_toggle_dispose (GObject *object) +{ + ECellTogglePrivate *priv; + + priv = E_CELL_TOGGLE_GET_PRIVATE (object); + + if (priv->empty != NULL) { + g_object_unref (priv->empty); + priv->empty = NULL; + } + + /* This unrefs all the elements. */ + g_ptr_array_set_size (priv->pixbufs, 0); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_cell_toggle_parent_class)->dispose (object); +} + +static void +cell_toggle_finalize (GObject *object) +{ + ECellTogglePrivate *priv; + guint ii; + + priv = E_CELL_TOGGLE_GET_PRIVATE (object); + + /* The array is not NULL-terminated, + * so g_strfreev() will not work. */ + for (ii = 0; ii < priv->n_icon_names; ii++) + g_free (priv->icon_names[ii]); + g_free (priv->icon_names); + + g_ptr_array_free (priv->pixbufs, TRUE); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_cell_toggle_parent_class)->finalize (object); +} + +static ECellView * +cell_toggle_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view) +{ + ECellToggleView *toggle_view = g_new0 (ECellToggleView, 1); + ETableItem *eti = E_TABLE_ITEM (e_table_item_view); + GnomeCanvas *canvas = GNOME_CANVAS_ITEM (eti)->canvas; + + toggle_view->cell_view.ecell = ecell; + toggle_view->cell_view.e_table_model = table_model; + toggle_view->cell_view.e_table_item_view = e_table_item_view; + toggle_view->cell_view.kill_view_cb = NULL; + toggle_view->cell_view.kill_view_cb_data = NULL; + toggle_view->canvas = canvas; + + return (ECellView *) toggle_view; +} + +static void +cell_toggle_kill_view (ECellView *ecell_view) +{ + ECellToggleView *toggle_view = (ECellToggleView *) ecell_view; + + if (toggle_view->cell_view.kill_view_cb) + toggle_view->cell_view.kill_view_cb ( + ecell_view, toggle_view->cell_view.kill_view_cb_data); + + if (toggle_view->cell_view.kill_view_cb_data) + g_list_free (toggle_view->cell_view.kill_view_cb_data); + + g_free (ecell_view); +} + +static void +cell_toggle_draw (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2) +{ + ECellTogglePrivate *priv; + GdkPixbuf *image; + gint x, y; + + const gint value = GPOINTER_TO_INT ( + e_table_model_value_at (ecell_view->e_table_model, model_col, row)); + + priv = E_CELL_TOGGLE_GET_PRIVATE (ecell_view->ecell); + + if (value < 0 || value >= priv->pixbufs->len) + return; + + image = g_ptr_array_index (priv->pixbufs, value); + + if ((x2 - x1) < gdk_pixbuf_get_width (image)) + x = x1; + else + x = x1 + ((x2 - x1) - gdk_pixbuf_get_width (image)) / 2; + + if ((y2 - y1) < gdk_pixbuf_get_height (image)) + y = y1; + else + y = y1 + ((y2 - y1) - gdk_pixbuf_get_height (image)) / 2; + + cairo_save (cr); + gdk_cairo_set_source_pixbuf (cr, image, x, y); + cairo_paint_with_alpha (cr, 1); + cairo_restore (cr); +} + +static void +etog_set_value (ECellToggleView *toggle_view, + gint model_col, + gint view_col, + gint row, + gint value) +{ + ECellTogglePrivate *priv; + + priv = E_CELL_TOGGLE_GET_PRIVATE (toggle_view->cell_view.ecell); + + if (value >= priv->pixbufs->len) + value = 0; + + e_table_model_set_value_at ( + toggle_view->cell_view.e_table_model, + model_col, row, GINT_TO_POINTER (value)); +} + +static gint +cell_toggle_event (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions) +{ + ECellToggleView *toggle_view = (ECellToggleView *) ecell_view; + gpointer _value = e_table_model_value_at ( + ecell_view->e_table_model, model_col, row); + const gint value = GPOINTER_TO_INT (_value); + + switch (event->type) { + case GDK_KEY_PRESS: + if (event->key.keyval != GDK_KEY_space) + return FALSE; + /* Fall through */ + case GDK_BUTTON_PRESS: + if (!e_table_model_is_cell_editable ( + ecell_view->e_table_model, model_col, row)) + return FALSE; + + etog_set_value ( + toggle_view, model_col, view_col, row, value + 1); + + return TRUE; + + default: + return FALSE; + } +} + +static gint +cell_toggle_height (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + ECellTogglePrivate *priv; + + priv = E_CELL_TOGGLE_GET_PRIVATE (ecell_view->ecell); + + return priv->height; +} + +static void +cell_toggle_print (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height) +{ + ECellTogglePrivate *priv; + GdkPixbuf *image; + gdouble image_width, image_height; + const gint value = GPOINTER_TO_INT ( + e_table_model_value_at (ecell_view->e_table_model, model_col, row)); + + cairo_t *cr; + + priv = E_CELL_TOGGLE_GET_PRIVATE (ecell_view->ecell); + + if (value >= priv->pixbufs->len) + return; + + image = g_ptr_array_index (priv->pixbufs, value); + if (image) { + cr = gtk_print_context_get_cairo_context (context); + cairo_save (cr); + cairo_translate (cr, 0 , 0); + image = gdk_pixbuf_add_alpha (image, TRUE, 255, 255, 255); + image_width = (gdouble) gdk_pixbuf_get_width (image); + image_height = (gdouble) gdk_pixbuf_get_height (image); + cairo_rectangle ( + cr, + image_width / 7, + image_height / 3, + image_width - image_width / 4, + image_width - image_height / 7); + cairo_clip (cr); + gdk_cairo_set_source_pixbuf (cr, image, 0, image_height / 4); + cairo_paint (cr); + cairo_restore (cr); + } +} + +static gdouble +cell_toggle_print_height (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width) +{ + ECellTogglePrivate *priv; + + priv = E_CELL_TOGGLE_GET_PRIVATE (ecell_view->ecell); + + return priv->height; +} + +static gint +cell_toggle_max_width (ECellView *ecell_view, + gint model_col, + gint view_col) +{ + ECellTogglePrivate *priv; + gint max_width = 0; + gint number_of_rows; + gint row; + + priv = E_CELL_TOGGLE_GET_PRIVATE (ecell_view->ecell); + + number_of_rows = e_table_model_row_count (ecell_view->e_table_model); + for (row = 0; row < number_of_rows; row++) { + GdkPixbuf *pixbuf; + gpointer value; + + value = e_table_model_value_at ( + ecell_view->e_table_model, model_col, row); + pixbuf = g_ptr_array_index ( + priv->pixbufs, GPOINTER_TO_INT (value)); + + max_width = MAX (max_width, gdk_pixbuf_get_width (pixbuf)); + } + + return max_width; +} + +static void +e_cell_toggle_class_init (ECellToggleClass *class) +{ + GObjectClass *object_class; + ECellClass *cell_class; + + g_type_class_add_private (class, sizeof (ECellTogglePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = cell_toggle_dispose; + object_class->finalize = cell_toggle_finalize; + + cell_class = E_CELL_CLASS (class); + cell_class->new_view = cell_toggle_new_view; + cell_class->kill_view = cell_toggle_kill_view; + cell_class->draw = cell_toggle_draw; + cell_class->event = cell_toggle_event; + cell_class->height = cell_toggle_height; + cell_class->print = cell_toggle_print; + cell_class->print_height = cell_toggle_print_height; + cell_class->max_width = cell_toggle_max_width; + + gal_a11y_e_cell_registry_add_cell_type ( + NULL, E_TYPE_CELL_TOGGLE, gal_a11y_e_cell_toggle_new); +} + +static void +e_cell_toggle_init (ECellToggle *cell_toggle) +{ + cell_toggle->priv = E_CELL_TOGGLE_GET_PRIVATE (cell_toggle); + + cell_toggle->priv->empty = + gdk_pixbuf_new_from_xpm_data (empty_xpm); + + cell_toggle->priv->pixbufs = + g_ptr_array_new_with_free_func (g_object_unref); +} + +/** + * e_cell_toggle_construct: + * @cell_toggle: a fresh ECellToggle object + * @icon_names: array of icon names, some of which may be %NULL + * @n_icon_names: length of the @icon_names array + * + * Constructs the @cell_toggle object with the @icon_names and @n_icon_names + * arguments. + */ +void +e_cell_toggle_construct (ECellToggle *cell_toggle, + const gchar **icon_names, + guint n_icon_names) +{ + guint ii; + + g_return_if_fail (E_IS_CELL_TOGGLE (cell_toggle)); + g_return_if_fail (icon_names != NULL); + g_return_if_fail (n_icon_names > 0); + + cell_toggle->priv->icon_names = g_new (gchar *, n_icon_names); + cell_toggle->priv->n_icon_names = n_icon_names; + + for (ii = 0; ii < n_icon_names; ii++) + cell_toggle->priv->icon_names[ii] = g_strdup (icon_names[ii]); + + cell_toggle_load_icons (cell_toggle); +} + +/** + * e_cell_toggle_new: + * @icon_names: array of icon names, some of which may be %NULL + * @n_icon_names: length of the @icon_names array + * + * Creates a new ECell renderer that can be used to render toggle + * buttons with the icons specified in @icon_names. The value returned + * by ETableModel::get_value is typecast into an integer and clamped + * to the [0..n_icon_names) range. That will select the image rendered. + * + * %NULL elements in @icon_names will show no icon for the corresponding + * integer value. + * + * Returns: an ECell object that can be used to render multi-state + * toggle cells. + */ +ECell * +e_cell_toggle_new (const gchar **icon_names, + guint n_icon_names) +{ + ECellToggle *cell_toggle; + + g_return_val_if_fail (icon_names != NULL, NULL); + g_return_val_if_fail (n_icon_names > 0, NULL); + + cell_toggle = g_object_new (E_TYPE_CELL_TOGGLE, NULL); + e_cell_toggle_construct (cell_toggle, icon_names, n_icon_names); + + return (ECell *) cell_toggle; +} + +GPtrArray * +e_cell_toggle_get_pixbufs (ECellToggle *cell_toggle) +{ + g_return_val_if_fail (E_IS_CELL_TOGGLE (cell_toggle), NULL); + + return cell_toggle->priv->pixbufs; +} diff --git a/e-util/e-cell-toggle.h b/e-util/e-cell-toggle.h new file mode 100644 index 0000000000..657836f142 --- /dev/null +++ b/e-util/e-cell-toggle.h @@ -0,0 +1,83 @@ +/* + * + * Multi-state image toggle cell object. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CELL_TOGGLE_H +#define E_CELL_TOGGLE_H + +#include <libgnomecanvas/libgnomecanvas.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include <e-util/e-cell.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_TOGGLE \ + (e_cell_toggle_get_type ()) +#define E_CELL_TOGGLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_TOGGLE, ECellToggle)) +#define E_CELL_TOGGLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_TOGGLE, ECellToggleClass)) +#define E_IS_CELL_TOGGLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_TOGGLE)) +#define E_IS_CELL_TOGGLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_TOGGLE)) +#define E_CELL_TOGGLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_TOGGLE, ECellToggleClass)) + +G_BEGIN_DECLS + +typedef struct _ECellToggle ECellToggle; +typedef struct _ECellToggleClass ECellToggleClass; +typedef struct _ECellTogglePrivate ECellTogglePrivate; + +struct _ECellToggle { + ECell parent; + ECellTogglePrivate *priv; +}; + +struct _ECellToggleClass { + ECellClass parent_class; +}; + +GType e_cell_toggle_get_type (void) G_GNUC_CONST; +ECell * e_cell_toggle_new (const gchar **icon_names, + guint n_icon_names); +void e_cell_toggle_construct (ECellToggle *cell_toggle, + const gchar **icon_names, + guint n_icon_names); +GPtrArray * e_cell_toggle_get_pixbufs (ECellToggle *cell_toggle); + +G_END_DECLS + +#endif /* E_CELL_TOGGLE_H */ + diff --git a/e-util/e-cell-tree.c b/e-util/e-cell-tree.c new file mode 100644 index 0000000000..085fb0cabe --- /dev/null +++ b/e-util/e-cell-tree.c @@ -0,0 +1,880 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * e-cell-tree.c - Tree cell object. + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * Authors: + * Chris Toshok <toshok@ximian.com> + * + * A majority of code taken from: + * + * the ECellText renderer. + * Copyright 1998, The Free Software Foundation + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <ctype.h> +#include <math.h> +#include <stdio.h> + +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include "gal-a11y-e-cell-registry.h" +#include "gal-a11y-e-cell-tree.h" + +#include "e-cell-tree.h" +#include "e-table-item.h" +#include "e-tree.h" +#include "e-tree-model.h" +#include "e-tree-table-adapter.h" + +G_DEFINE_TYPE (ECellTree, e_cell_tree, E_TYPE_CELL) + +typedef struct { + ECellView cell_view; + ECellView *subcell_view; + + GnomeCanvas *canvas; + gboolean prelit; + gint animate_timeout; + +} ECellTreeView; + +#define INDENT_AMOUNT 16 + +ECellView * +e_cell_tree_view_get_subcell_view (ECellView *ect) +{ + return ((ECellTreeView *) ect)->subcell_view; +} + +static ETreePath +e_cell_tree_get_node (ETableModel *table_model, + gint row) +{ + return e_table_model_value_at (table_model, -1, row); +} + +static ETreeModel * +e_cell_tree_get_tree_model (ETableModel *table_model, + gint row) +{ + return e_table_model_value_at (table_model, -2, row); +} + +static ETreeTableAdapter * +e_cell_tree_get_tree_table_adapter (ETableModel *table_model, + gint row) +{ + return e_table_model_value_at (table_model, -3, row); +} + +static gint +visible_depth_of_node (ETableModel *model, + gint row) +{ + ETreeModel *tree_model = e_cell_tree_get_tree_model (model, row); + ETreeTableAdapter *adapter = e_cell_tree_get_tree_table_adapter (model, row); + ETreePath path = e_cell_tree_get_node (model, row); + return (e_tree_model_node_depth (tree_model, path) + - (e_tree_table_adapter_root_node_is_visible (adapter) ? 0 : 1)); +} + +/* If this is changed to not include the width of the expansion pixmap + * if the path is not expandable, then max_width needs to change as + * well. */ +static gint +offset_of_node (ETableModel *table_model, + gint row) +{ + ETreeModel *tree_model = e_cell_tree_get_tree_model (table_model, row); + ETreePath path = e_cell_tree_get_node (table_model, row); + + if (visible_depth_of_node (table_model, row) >= 0 || + e_tree_model_node_is_expandable (tree_model, path)) { + return (visible_depth_of_node (table_model, row) + 1) * INDENT_AMOUNT; + } else { + return 0; + } +} + +/* + * ECell::new_view method + */ +static ECellView * +ect_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view) +{ + ECellTree *ect = E_CELL_TREE (ecell); + ECellTreeView *tree_view = g_new0 (ECellTreeView, 1); + GnomeCanvas *canvas = GNOME_CANVAS_ITEM (e_table_item_view)->canvas; + + tree_view->cell_view.ecell = ecell; + tree_view->cell_view.e_table_model = table_model; + tree_view->cell_view.e_table_item_view = e_table_item_view; + tree_view->cell_view.kill_view_cb = NULL; + tree_view->cell_view.kill_view_cb_data = NULL; + + /* create our subcell view */ + tree_view->subcell_view = e_cell_new_view (ect->subcell, table_model, e_table_item_view /* XXX */); + + tree_view->canvas = canvas; + + return (ECellView *) tree_view; +} + +/* + * ECell::kill_view method + */ +static void +ect_kill_view (ECellView *ecv) +{ + ECellTreeView *tree_view = (ECellTreeView *) ecv; + + if (tree_view->cell_view.kill_view_cb) + (tree_view->cell_view.kill_view_cb)(ecv, tree_view->cell_view.kill_view_cb_data); + + if (tree_view->cell_view.kill_view_cb_data) + g_list_free (tree_view->cell_view.kill_view_cb_data); + + /* kill our subcell view */ + e_cell_kill_view (tree_view->subcell_view); + + g_free (tree_view); +} + +/* + * ECell::realize method + */ +static void +ect_realize (ECellView *ecell_view) +{ + ECellTreeView *tree_view = (ECellTreeView *) ecell_view; + + /* realize our subcell view */ + e_cell_realize (tree_view->subcell_view); + + if (E_CELL_CLASS (e_cell_tree_parent_class)->realize) + (* E_CELL_CLASS (e_cell_tree_parent_class)->realize) (ecell_view); +} + +/* + * ECell::unrealize method + */ +static void +ect_unrealize (ECellView *ecv) +{ + ECellTreeView *tree_view = (ECellTreeView *) ecv; + + /* unrealize our subcell view. */ + e_cell_unrealize (tree_view->subcell_view); + + if (E_CELL_CLASS (e_cell_tree_parent_class)->unrealize) + (* E_CELL_CLASS (e_cell_tree_parent_class)->unrealize) (ecv); +} + +static void +draw_expander (ECellTreeView *ectv, + cairo_t *cr, + GtkExpanderStyle expander_style, + GtkStateType state, + GdkRectangle *rect) +{ + GtkStyleContext *style_context; + GtkWidget *tree; + GtkStateFlags flags = 0; + gint exp_size; + + tree = gtk_widget_get_parent (GTK_WIDGET (ectv->canvas)); + style_context = gtk_widget_get_style_context (tree); + + gtk_style_context_save (style_context); + + gtk_style_context_add_class (style_context, GTK_STYLE_CLASS_EXPANDER); + + switch (state) { + case GTK_STATE_PRELIGHT: + flags |= GTK_STATE_FLAG_PRELIGHT; + break; + case GTK_STATE_SELECTED: + flags |= GTK_STATE_FLAG_SELECTED; + break; + case GTK_STATE_INSENSITIVE: + flags |= GTK_STATE_FLAG_INSENSITIVE; + break; + default: + break; + } + + if (expander_style == GTK_EXPANDER_EXPANDED) + flags |= GTK_STATE_FLAG_ACTIVE; + + gtk_style_context_set_state (style_context, flags); + + gtk_widget_style_get (tree, "expander_size", &exp_size, NULL); + + cairo_save (cr); + + gtk_render_expander ( + style_context, cr, + (gdouble) rect->x + rect->width - exp_size, + (gdouble) (rect->y + rect->height / 2) - (exp_size / 2), + (gdouble) exp_size, + (gdouble) exp_size); + + cairo_restore (cr); + + gtk_style_context_restore (style_context); +} + +/* + * ECell::draw method + */ +static void +ect_draw (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2) +{ + ECellTreeView *tree_view = (ECellTreeView *) ecell_view; + ETreeModel *tree_model = e_cell_tree_get_tree_model (ecell_view->e_table_model, row); + ETreeTableAdapter *tree_table_adapter = e_cell_tree_get_tree_table_adapter (ecell_view->e_table_model, row); + ETreePath node; + GdkRectangle rect; + gint offset, subcell_offset; + + cairo_save (cr); + + /* only draw the tree effects if we're the active sort */ + if (/* XXX */ TRUE) { + GdkPixbuf *node_image; + gint node_image_width = 0, node_image_height = 0; + + tree_view->prelit = FALSE; + + node = e_cell_tree_get_node (ecell_view->e_table_model, row); + + offset = offset_of_node (ecell_view->e_table_model, row); + subcell_offset = offset; + + node_image = e_tree_model_icon_at (tree_model, node); + + if (node_image) { + node_image_width = gdk_pixbuf_get_width (node_image); + node_image_height = gdk_pixbuf_get_height (node_image); + } + + /* + * Be a nice citizen: clip to the region we are supposed to draw on + */ + rect.x = x1; + rect.y = y1; + rect.width = subcell_offset + node_image_width; + rect.height = y2 - y1; + + /* now draw our icon if we're expandable */ + if (e_tree_model_node_is_expandable (tree_model, node)) { + gboolean expanded = e_tree_table_adapter_node_is_expanded (tree_table_adapter, node); + GdkRectangle r; + + r = rect; + r.width -= node_image_width + 2; + draw_expander (tree_view, cr, expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED, GTK_STATE_NORMAL, &r); + } + + if (node_image) { + gdk_cairo_set_source_pixbuf ( + cr, node_image, + x1 + subcell_offset, + y1 + (y2 - y1) / 2 - node_image_height / 2); + cairo_paint (cr); + + subcell_offset += node_image_width; + } + } + + /* Now cause our subcell to draw its contents, shifted by + * subcell_offset pixels */ + e_cell_draw ( + tree_view->subcell_view, cr, + model_col, view_col, row, flags, + x1 + subcell_offset, y1, x2, y2); + + cairo_restore (cr); +} + +static void +adjust_event_position (GdkEvent *event, + gint offset) +{ + switch (event->type) { + case GDK_BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + event->button.x += offset; + break; + case GDK_MOTION_NOTIFY: + event->motion.x += offset; + break; + default: + break; + } +} + +static gboolean +event_in_expander (GdkEvent *event, + gint offset, + gint height) +{ + switch (event->type) { + case GDK_BUTTON_PRESS: + return (event->button.x > (offset - INDENT_AMOUNT) && event->button.x < offset); + case GDK_MOTION_NOTIFY: + return (event->motion.x > (offset - INDENT_AMOUNT) && event->motion.x < offset && + event->motion.y > 2 && event->motion.y < (height - 2)); + default: + break; + } + + return FALSE; +} + +/* + * ECell::height method + */ +static gint +ect_height (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + ECellTreeView *tree_view = (ECellTreeView *) ecell_view; + + return (((e_cell_height (tree_view->subcell_view, model_col, view_col, row)) + 1) / 2) * 2; +} + +typedef struct { + ECellTreeView *ectv; + ETreeTableAdapter *etta; + ETreePath node; + gboolean expanded; + gboolean finish; + GdkRectangle area; +} animate_closure_t; + +static gboolean +animate_expander (gpointer data) +{ + GtkLayout *layout; + GdkWindow *window; + animate_closure_t *closure = (animate_closure_t *) data; + cairo_t *cr; + + if (closure->finish) { + e_tree_table_adapter_node_set_expanded (closure->etta, closure->node, !closure->expanded); + closure->ectv->animate_timeout = 0; + g_free (data); + return FALSE; + } + + layout = GTK_LAYOUT (closure->ectv->canvas); + window = gtk_layout_get_bin_window (layout); + + cr = gdk_cairo_create (window); + + draw_expander ( + closure->ectv, cr, closure->expanded ? + GTK_EXPANDER_SEMI_COLLAPSED : + GTK_EXPANDER_SEMI_EXPANDED, + GTK_STATE_NORMAL, &closure->area); + closure->finish = TRUE; + + cairo_destroy (cr); + + return TRUE; +} + +/* + * ECell::event method + */ +static gint +ect_event (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions) +{ + GtkLayout *layout; + GdkWindow *window; + ECellTreeView *tree_view = (ECellTreeView *) ecell_view; + ETreeModel *tree_model = e_cell_tree_get_tree_model (ecell_view->e_table_model, row); + ETreeTableAdapter *etta = e_cell_tree_get_tree_table_adapter (ecell_view->e_table_model, row); + ETreePath node = e_cell_tree_get_node (ecell_view->e_table_model, row); + gint offset = offset_of_node (ecell_view->e_table_model, row); + gint result; + + layout = GTK_LAYOUT (tree_view->canvas); + window = gtk_layout_get_bin_window (layout); + + switch (event->type) { + case GDK_BUTTON_PRESS: + + if (event_in_expander (event, offset, 0)) { + if (e_tree_model_node_is_expandable (tree_model, node)) { + gboolean expanded = e_tree_table_adapter_node_is_expanded (etta, node); + gint tmp_row = row; + GdkRectangle area; + animate_closure_t *closure = g_new0 (animate_closure_t, 1); + cairo_t *cr; + gint hgt; + + e_table_item_get_cell_geometry ( + tree_view->cell_view.e_table_item_view, + &tmp_row, &view_col, &area.x, &area.y, NULL, &area.height); + area.width = offset - 2; + hgt = e_cell_height (ecell_view, model_col, view_col, row); + + if (hgt != area.height) /* Composite cells */ + area.height += hgt; + + cr = gdk_cairo_create (window); + draw_expander ( + tree_view, cr, expanded ? + GTK_EXPANDER_SEMI_EXPANDED : + GTK_EXPANDER_SEMI_COLLAPSED, + GTK_STATE_NORMAL, &area); + cairo_destroy (cr); + + closure->ectv = tree_view; + closure->etta = etta; + closure->node = node; + closure->expanded = expanded; + closure->area = area; + tree_view->animate_timeout = g_timeout_add (50, animate_expander, closure); + return TRUE; + } + } + else if (event->button.x < (offset - INDENT_AMOUNT)) + return FALSE; + break; + + case GDK_MOTION_NOTIFY: + + if (e_tree_model_node_is_expandable (tree_model, node)) { + gint height = ect_height (ecell_view, model_col, view_col, row); + GdkRectangle area; + gboolean in_expander = event_in_expander (event, offset, height); + + if (tree_view->prelit ^ in_expander) { + gint tmp_row = row; + cairo_t *cr; + + e_table_item_get_cell_geometry ( + tree_view->cell_view.e_table_item_view, + &tmp_row, &view_col, &area.x, &area.y, NULL, &area.height); + area.width = offset - 2; + + cr = gdk_cairo_create (window); + draw_expander ( + tree_view, cr, + e_tree_table_adapter_node_is_expanded (etta, node) ? + GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED, + in_expander ? GTK_STATE_PRELIGHT : GTK_STATE_NORMAL, &area); + cairo_destroy (cr); + + tree_view->prelit = in_expander; + return TRUE; + } + + } + break; + + case GDK_LEAVE_NOTIFY: + + if (tree_view->prelit) { + gint tmp_row = row; + GdkRectangle area; + cairo_t *cr; + + e_table_item_get_cell_geometry ( + tree_view->cell_view.e_table_item_view, + &tmp_row, &view_col, &area.x, &area.y, NULL, &area.height); + area.width = offset - 2; + + cr = gdk_cairo_create (window); + draw_expander ( + tree_view, cr, + e_tree_table_adapter_node_is_expanded (etta, node) ? + GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED, + GTK_STATE_NORMAL, &area); + cairo_destroy (cr); + + tree_view->prelit = FALSE; + } + return TRUE; + + default: + break; + } + + adjust_event_position (event, -offset); + result = e_cell_event (tree_view->subcell_view, event, model_col, view_col, row, flags, actions); + adjust_event_position (event, offset); + + return result; +} + +/* + * ECell::max_width method + */ +static gint +ect_max_width (ECellView *ecell_view, + gint model_col, + gint view_col) +{ + ECellTreeView *tree_view = (ECellTreeView *) ecell_view; + gint row; + gint number_of_rows; + gint max_width = 0; + gint width = 0; + gint subcell_max_width = 0; + gboolean per_row = e_cell_max_width_by_row_implemented (tree_view->subcell_view); + + number_of_rows = e_table_model_row_count (ecell_view->e_table_model); + + if (!per_row) + subcell_max_width = e_cell_max_width (tree_view->subcell_view, model_col, view_col); + + for (row = 0; row < number_of_rows; row++) { + ETreeModel *tree_model = e_cell_tree_get_tree_model (ecell_view->e_table_model, row); + ETreePath node; + GdkPixbuf *node_image; + gint node_image_width = 0; + + gint offset, subcell_offset; +#if 0 + gboolean expanded, expandable; + ETreeTableAdapter *tree_table_adapter = e_cell_tree_get_tree_table_adapter (ecell_view->e_table_model, row); +#endif + + node = e_cell_tree_get_node (ecell_view->e_table_model, row); + + offset = offset_of_node (ecell_view->e_table_model, row); + subcell_offset = offset; + + node_image = e_tree_model_icon_at (tree_model, node); + + if (node_image) { + node_image_width = gdk_pixbuf_get_width (node_image); + } + + width = subcell_offset + node_image_width; + + if (per_row) + width += e_cell_max_width_by_row (tree_view->subcell_view, model_col, view_col, row); + else + width += subcell_max_width; + +#if 0 + expandable = e_tree_model_node_is_expandable (tree_model, node); + expanded = e_tree_table_adapter_node_is_expanded (tree_table_adapter, node); + + /* This is unnecessary since this is already handled + * by the offset_of_node function. If that changes, + * this will have to change too. */ + + if (expandable) { + GdkPixbuf *image; + + image = (expanded + ? E_CELL_TREE (tree_view->cell_view.ecell)->open_pixbuf + : E_CELL_TREE (tree_view->cell_view.ecell)->closed_pixbuf); + + width += gdk_pixbuf_get_width (image); + } +#endif + + max_width = MAX (max_width, width); + } + + return max_width; +} + +/* + * ECellView::get_bg_color method + */ +static gchar * +ect_get_bg_color (ECellView *ecell_view, + gint row) +{ + ECellTreeView *tree_view = (ECellTreeView *) ecell_view; + + return e_cell_get_bg_color (tree_view->subcell_view, row); +} + +/* + * ECellView::enter_edit method + */ +static gpointer +ect_enter_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + /* just defer to our subcell's view */ + ECellTreeView *tree_view = (ECellTreeView *) ecell_view; + + return e_cell_enter_edit (tree_view->subcell_view, model_col, view_col, row); +} + +/* + * ECellView::leave_edit method + */ +static void +ect_leave_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context) +{ + /* just defer to our subcell's view */ + ECellTreeView *tree_view = (ECellTreeView *) ecell_view; + + e_cell_leave_edit (tree_view->subcell_view, model_col, view_col, row, edit_context); +} + +static void +ect_print (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height) +{ + ECellTreeView *tree_view = (ECellTreeView *) ecell_view; + cairo_t *cr = gtk_print_context_get_cairo_context (context); + + cairo_save (cr); + + if (/* XXX only if we're the active sort */ TRUE) { + ETreeModel *tree_model = e_cell_tree_get_tree_model (ecell_view->e_table_model, row); + ETreeTableAdapter *tree_table_adapter = e_cell_tree_get_tree_table_adapter (ecell_view->e_table_model, row); + ETreePath node = e_cell_tree_get_node (ecell_view->e_table_model, row); + gint offset = offset_of_node (ecell_view->e_table_model, row); + gint subcell_offset = offset; + gboolean expandable = e_tree_model_node_is_expandable (tree_model, node); + + /* draw our lines */ + if (E_CELL_TREE (tree_view->cell_view.ecell)->draw_lines) { + gint depth; + + if (!e_tree_model_node_is_root (tree_model, node) + || e_tree_model_node_get_children (tree_model, node, NULL) > 0) { + cairo_move_to ( + cr, + offset - INDENT_AMOUNT / 2, + height / 2); + cairo_line_to (cr, offset, height / 2); + } + + if (visible_depth_of_node (ecell_view->e_table_model, row) != 0) { + cairo_move_to ( + cr, + offset - INDENT_AMOUNT / 2, height); + cairo_line_to ( + cr, + offset - INDENT_AMOUNT / 2, + e_tree_table_adapter_node_get_next + (tree_table_adapter, node) ? 0 : + height / 2); + } + + /* now traverse back up to the root of the tree, checking at + * each level if the node has siblings, and drawing the + * correct vertical pipe for it's configuration. */ + node = e_tree_model_node_get_parent (tree_model, node); + depth = visible_depth_of_node (ecell_view->e_table_model, row) - 1; + offset -= INDENT_AMOUNT; + while (node && depth != 0) { + if (e_tree_table_adapter_node_get_next (tree_table_adapter, node)) { + cairo_move_to ( + cr, + offset - INDENT_AMOUNT / 2, + height); + cairo_line_to ( + cr, + offset - INDENT_AMOUNT / 2, 0); + } + node = e_tree_model_node_get_parent (tree_model, node); + depth--; + offset -= INDENT_AMOUNT; + } + } + + /* now draw our icon if we're expandable */ + if (expandable) { + gboolean expanded; + GdkRectangle r; + gint exp_size = 0; + + gtk_widget_style_get (GTK_WIDGET (gtk_widget_get_parent (GTK_WIDGET (tree_view->canvas))), "expander_size", &exp_size, NULL); + + node = e_cell_tree_get_node (ecell_view->e_table_model, row); + expanded = e_tree_table_adapter_node_is_expanded (tree_table_adapter, node); + + r.x = 0; + r.y = 0; + r.width = MIN (width, exp_size); + r.height = height; + + draw_expander (tree_view, cr, expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED, GTK_STATE_NORMAL, &r); + } + + cairo_stroke (cr); + + cairo_translate (cr, subcell_offset, 0); + width -= subcell_offset; + } + + cairo_restore (cr); + + e_cell_print (tree_view->subcell_view, context, model_col, view_col, row, width, height); +} + +static gdouble +ect_print_height (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width) +{ + return 12; /* XXX */ +} + +/* + * GObject::dispose method + */ +static void +ect_dispose (GObject *object) +{ + ECellTree *ect = E_CELL_TREE (object); + + /* destroy our subcell */ + if (ect->subcell) + g_object_unref (ect->subcell); + ect->subcell = NULL; + + G_OBJECT_CLASS (e_cell_tree_parent_class)->dispose (object); +} + +static void +e_cell_tree_class_init (ECellTreeClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + ECellClass *ecc = E_CELL_CLASS (class); + + object_class->dispose = ect_dispose; + + ecc->new_view = ect_new_view; + ecc->kill_view = ect_kill_view; + ecc->realize = ect_realize; + ecc->unrealize = ect_unrealize; + ecc->draw = ect_draw; + ecc->event = ect_event; + ecc->height = ect_height; + ecc->enter_edit = ect_enter_edit; + ecc->leave_edit = ect_leave_edit; + ecc->print = ect_print; + ecc->print_height = ect_print_height; + ecc->max_width = ect_max_width; + ecc->get_bg_color = ect_get_bg_color; + + gal_a11y_e_cell_registry_add_cell_type (NULL, E_TYPE_CELL_TREE, gal_a11y_e_cell_tree_new); +} + +static void +e_cell_tree_init (ECellTree *ect) +{ + /* nothing to do */ +} + +/** + * e_cell_tree_construct: + * @ect: the ECellTree we're constructing. + * @draw_lines: whether or not to draw the lines between parents/children/siblings. + * @subcell: the ECell to render to the right of the tree effects. + * + * Constructs an ECellTree. used by subclasses that need to + * initialize a nested ECellTree. See e_cell_tree_new() for more info. + * + **/ +void +e_cell_tree_construct (ECellTree *ect, + gboolean draw_lines, + ECell *subcell) +{ + ect->subcell = subcell; + if (subcell) + g_object_ref_sink (subcell); + + ect->draw_lines = draw_lines; +} + +/** + * e_cell_tree_new: + * @draw_lines: whether or not to draw the lines between parents/children/siblings. + * @subcell: the ECell to render to the right of the tree effects. + * + * Creates a new ECell renderer that can be used to render tree + * effects that come from an ETreeModel. Various assumptions are made + * as to the fact that the ETableModel the ETable this cell is + * associated with is in fact an ETreeModel. The cell uses special + * columns to get at structural information (needed to draw the + * lines/icons. + * + * Return value: an ECell object that can be used to render trees. + **/ +ECell * +e_cell_tree_new (gboolean draw_lines, + ECell *subcell) +{ + ECellTree *ect = g_object_new (E_TYPE_CELL_TREE, NULL); + + e_cell_tree_construct (ect, draw_lines, subcell); + + return (ECell *) ect; +} + diff --git a/e-util/e-cell-tree.h b/e-util/e-cell-tree.h new file mode 100644 index 0000000000..044c14bfed --- /dev/null +++ b/e-util/e-cell-tree.h @@ -0,0 +1,90 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * e-cell-tree.h - Tree cell object. + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * Authors: + * Chris Toshok <toshok@ximian.com> + * + * A majority of code taken from: + * + * the ECellText renderer. + * Copyright 1998, The Free Software Foundation + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_TREE_H_ +#define _E_CELL_TREE_H_ + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-cell.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_TREE \ + (e_cell_tree_get_type ()) +#define E_CELL_TREE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_TREE, ECellTree)) +#define E_CELL_TREE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_TREE, ECellTreeClass)) +#define E_IS_CELL_TREE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_TREE)) +#define E_IS_CELL_TREE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_TREE)) +#define E_CELL_TREE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_TREE, ECellTreeClass)) + +G_BEGIN_DECLS + +typedef struct _ECellTree ECellTree; +typedef struct _ECellTreeClass ECellTreeClass; + +struct _ECellTree { + ECell parent; + + gboolean draw_lines; + + ECell *subcell; +}; + +struct _ECellTreeClass { + ECellClass parent_class; +}; + +GType e_cell_tree_get_type (void) G_GNUC_CONST; +ECell * e_cell_tree_new (gboolean draw_lines, + ECell *subcell); +void e_cell_tree_construct (ECellTree *ect, + gboolean draw_lines, + ECell *subcell); +ECellView * e_cell_tree_view_get_subcell_view + (ECellView *ect); + +G_END_DECLS + +#endif /* _E_CELL_TREE_H_ */ + diff --git a/e-util/e-cell-vbox.c b/e-util/e-cell-vbox.c new file mode 100644 index 0000000000..ef34a0a097 --- /dev/null +++ b/e-util/e-cell-vbox.c @@ -0,0 +1,341 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Toshok <toshok@ximian.com> + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <ctype.h> +#include <math.h> +#include <stdio.h> + +#include <gtk/gtk.h> + +#include "gal-a11y-e-cell-registry.h" +#include "gal-a11y-e-cell-vbox.h" + +#include "e-cell-vbox.h" +#include "e-table-item.h" + +G_DEFINE_TYPE (ECellVbox, e_cell_vbox, E_TYPE_CELL) + +#define INDENT_AMOUNT 16 + +/* + * ECell::new_view method + */ +static ECellView * +ecv_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view) +{ + ECellVbox *ecv = E_CELL_VBOX (ecell); + ECellVboxView *vbox_view = g_new0 (ECellVboxView, 1); + gint i; + + vbox_view->cell_view.ecell = ecell; + vbox_view->cell_view.e_table_model = table_model; + vbox_view->cell_view.e_table_item_view = e_table_item_view; + vbox_view->cell_view.kill_view_cb = NULL; + vbox_view->cell_view.kill_view_cb_data = NULL; + + /* create our subcell view */ + vbox_view->subcell_view_count = ecv->subcell_count; + vbox_view->subcell_views = g_new (ECellView *, vbox_view->subcell_view_count); + vbox_view->model_cols = g_new (int, vbox_view->subcell_view_count); + + for (i = 0; i < vbox_view->subcell_view_count; i++) { + vbox_view->subcell_views[i] = e_cell_new_view (ecv->subcells[i], table_model, e_table_item_view /* XXX */); + vbox_view->model_cols[i] = ecv->model_cols[i]; + } + + return (ECellView *) vbox_view; +} + +/* + * ECell::kill_view method + */ +static void +ecv_kill_view (ECellView *ecv) +{ + ECellVboxView *vbox_view = (ECellVboxView *) ecv; + gint i; + + if (vbox_view->cell_view.kill_view_cb) + (vbox_view->cell_view.kill_view_cb)(ecv, vbox_view->cell_view.kill_view_cb_data); + + if (vbox_view->cell_view.kill_view_cb_data) + g_list_free (vbox_view->cell_view.kill_view_cb_data); + + /* kill our subcell view */ + for (i = 0; i < vbox_view->subcell_view_count; i++) + e_cell_kill_view (vbox_view->subcell_views[i]); + + g_free (vbox_view->model_cols); + g_free (vbox_view->subcell_views); + g_free (vbox_view); +} + +/* + * ECell::realize method + */ +static void +ecv_realize (ECellView *ecell_view) +{ + ECellVboxView *vbox_view = (ECellVboxView *) ecell_view; + gint i; + + /* realize our subcell view */ + for (i = 0; i < vbox_view->subcell_view_count; i++) + e_cell_realize (vbox_view->subcell_views[i]); + + if (E_CELL_CLASS (e_cell_vbox_parent_class)->realize) + (* E_CELL_CLASS (e_cell_vbox_parent_class)->realize) (ecell_view); +} + +/* + * ECell::unrealize method + */ +static void +ecv_unrealize (ECellView *ecv) +{ + ECellVboxView *vbox_view = (ECellVboxView *) ecv; + gint i; + + /* unrealize our subcell view. */ + for (i = 0; i < vbox_view->subcell_view_count; i++) + e_cell_unrealize (vbox_view->subcell_views[i]); + + if (E_CELL_CLASS (e_cell_vbox_parent_class)->unrealize) + (* E_CELL_CLASS (e_cell_vbox_parent_class)->unrealize) (ecv); +} + +/* + * ECell::draw method + */ +static void +ecv_draw (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2) +{ + ECellVboxView *vbox_view = (ECellVboxView *) ecell_view; + + gint subcell_offset = 0; + gint i; + + for (i = 0; i < vbox_view->subcell_view_count; i++) { + /* Now cause our subcells to draw their contents, + * shifted by subcell_offset pixels */ + gint height; + + height = e_cell_height ( + vbox_view->subcell_views[i], + vbox_view->model_cols[i], view_col, row); + e_cell_draw ( + vbox_view->subcell_views[i], cr, + vbox_view->model_cols[i], view_col, row, flags, + x1, y1 + subcell_offset, x2, + y1 + subcell_offset + height); + + subcell_offset += e_cell_height ( + vbox_view->subcell_views[i], + vbox_view->model_cols[i], view_col, row); + } +} + +/* + * ECell::event method + */ +static gint +ecv_event (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions) +{ + ECellVboxView *vbox_view = (ECellVboxView *) ecell_view; + gint y = 0; + gint i; + gint subcell_offset = 0; + + switch (event->type) { + case GDK_BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + y = event->button.y; + break; + case GDK_MOTION_NOTIFY: + y = event->motion.y; + break; + default: + /* nada */ + break; + } + + for (i = 0; i < vbox_view->subcell_view_count; i++) { + gint height = e_cell_height (vbox_view->subcell_views[i], vbox_view->model_cols[i], view_col, row); + if (y < subcell_offset + height) + return e_cell_event (vbox_view->subcell_views[i], event, vbox_view->model_cols[i], view_col, row, flags, actions); + subcell_offset += height; + } + return 0; +} + +/* + * ECell::height method + */ +static gint +ecv_height (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + ECellVboxView *vbox_view = (ECellVboxView *) ecell_view; + gint height = 0; + gint i; + + for (i = 0; i < vbox_view->subcell_view_count; i++) { + height += e_cell_height (vbox_view->subcell_views[i], vbox_view->model_cols[i], view_col, row); + } + return height; +} + +/* + * ECell::max_width method + */ +static gint +ecv_max_width (ECellView *ecell_view, + gint model_col, + gint view_col) +{ + ECellVboxView *vbox_view = (ECellVboxView *) ecell_view; + gint max_width = 0; + gint i; + + for (i = 0; i < vbox_view->subcell_view_count; i++) { + gint width = e_cell_max_width (vbox_view->subcell_views[i], vbox_view->model_cols[i], view_col); + max_width = MAX (width, max_width); + } + + return max_width; +} + +/* + * GObject::dispose method + */ +static void +ecv_dispose (GObject *object) +{ + ECellVbox *ecv = E_CELL_VBOX (object); + gint i; + + /* destroy our subcell */ + for (i = 0; i < ecv->subcell_count; i++) + if (ecv->subcells[i]) + g_object_unref (ecv->subcells[i]); + g_free (ecv->subcells); + ecv->subcells = NULL; + ecv->subcell_count = 0; + + G_OBJECT_CLASS (e_cell_vbox_parent_class)->dispose (object); +} + +static void +ecv_finalize (GObject *object) +{ + ECellVbox *ecv = E_CELL_VBOX (object); + + g_free (ecv->model_cols); + + G_OBJECT_CLASS (e_cell_vbox_parent_class)->finalize (object); +} + +static void +e_cell_vbox_class_init (ECellVboxClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + ECellClass *ecc = E_CELL_CLASS (class); + + object_class->dispose = ecv_dispose; + object_class->finalize = ecv_finalize; + + ecc->new_view = ecv_new_view; + ecc->kill_view = ecv_kill_view; + ecc->realize = ecv_realize; + ecc->unrealize = ecv_unrealize; + ecc->draw = ecv_draw; + ecc->event = ecv_event; + ecc->height = ecv_height; + ecc->max_width = ecv_max_width; + + gal_a11y_e_cell_registry_add_cell_type (NULL, E_TYPE_CELL_VBOX, gal_a11y_e_cell_vbox_new); +} + +static void +e_cell_vbox_init (ECellVbox *ecv) +{ + ecv->subcells = NULL; + ecv->subcell_count = 0; +} + +/** + * e_cell_vbox_new: + * + * Creates a new ECell renderer that can be used to render multiple + * child cells. + * + * Return value: an ECell object that can be used to render multiple + * child cells. + **/ +ECell * +e_cell_vbox_new (void) +{ + return g_object_new (E_TYPE_CELL_VBOX, NULL); +} + +void +e_cell_vbox_append (ECellVbox *vbox, + ECell *subcell, + gint model_col) +{ + vbox->subcell_count++; + + vbox->subcells = g_renew (ECell *, vbox->subcells, vbox->subcell_count); + vbox->model_cols = g_renew (int, vbox->model_cols, vbox->subcell_count); + + vbox->subcells[vbox->subcell_count - 1] = subcell; + vbox->model_cols[vbox->subcell_count - 1] = model_col; + + if (subcell) + g_object_ref_sink (subcell); +} diff --git a/e-util/e-cell-vbox.h b/e-util/e-cell-vbox.h new file mode 100644 index 0000000000..690d78f7d9 --- /dev/null +++ b/e-util/e-cell-vbox.h @@ -0,0 +1,93 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Toshok <toshok@ximian.com> + * Chris Lahey <clahey@ximina.com + * + * A majority of code taken from: + * the ECellText renderer. + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_VBOX_H_ +#define _E_CELL_VBOX_H_ + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-cell.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL_VBOX \ + (e_cell_vbox_get_type ()) +#define E_CELL_VBOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL_VBOX, ECellVbox)) +#define E_CELL_VBOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL_VBOX, ECellVboxClass)) +#define E_IS_CELL_VBOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL_VBOX)) +#define E_IS_CELL_VBOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL_VBOX)) +#define E_CELL_VBOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL_VBOX, ECellVboxClass)) + +G_BEGIN_DECLS + +typedef struct _ECellVbox ECellVbox; +typedef struct _ECellVboxView ECellVboxView; +typedef struct _ECellVboxClass ECellVboxClass; + +struct _ECellVbox { + ECell parent; + + gint subcell_count; + ECell **subcells; + gint *model_cols; +}; + +struct _ECellVboxView { + ECellView cell_view; + + gint subcell_view_count; + ECellView **subcell_views; + gint *model_cols; +}; + +struct _ECellVboxClass { + ECellClass parent_class; +}; + +GType e_cell_vbox_get_type (void) G_GNUC_CONST; +ECell * e_cell_vbox_new (void); +void e_cell_vbox_append (ECellVbox *vbox, + ECell *subcell, + gint model_col); + +G_END_DECLS + +#endif /* _E_CELL_VBOX_H_ */ diff --git a/e-util/e-cell.c b/e-util/e-cell.c new file mode 100644 index 0000000000..34046e1b38 --- /dev/null +++ b/e-util/e-cell.c @@ -0,0 +1,679 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> + +#include "e-cell.h" + +G_DEFINE_TYPE (ECell, e_cell, G_TYPE_OBJECT) + +static ECellView * +ec_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view) +{ + return NULL; +} + +static void +ec_realize (ECellView *e_cell) +{ +} + +static void +ec_kill_view (ECellView *ecell_view) +{ +} + +static void +ec_unrealize (ECellView *e_cell) +{ +} + +static void +ec_draw (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2) +{ + g_critical ("e-cell-draw invoked"); +} + +static gint +ec_event (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions) +{ + g_critical ("e-cell-event invoked"); + + return 0; +} + +static gint +ec_height (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + g_critical ("e-cell-height invoked"); + + return 0; +} + +static void +ec_focus (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gint x1, + gint y1, + gint x2, + gint y2) +{ + ecell_view->focus_col = view_col; + ecell_view->focus_row = row; + ecell_view->focus_x1 = x1; + ecell_view->focus_y1 = y1; + ecell_view->focus_x2 = x2; + ecell_view->focus_y2 = y2; +} + +static void +ec_unfocus (ECellView *ecell_view) +{ + ecell_view->focus_col = -1; + ecell_view->focus_row = -1; + ecell_view->focus_x1 = -1; + ecell_view->focus_y1 = -1; + ecell_view->focus_x2 = -1; + ecell_view->focus_y2 = -1; +} + +static gpointer +ec_enter_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + return NULL; +} + +static void +ec_leave_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer context) +{ +} + +static gpointer +ec_save_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer context) +{ + return NULL; +} + +static void +ec_load_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer context, + gpointer save_state) +{ +} + +static void +ec_free_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer save_state) +{ +} + +static void +e_cell_class_init (ECellClass *class) +{ + class->realize = ec_realize; + class->unrealize = ec_unrealize; + class->new_view = ec_new_view; + class->kill_view = ec_kill_view; + class->draw = ec_draw; + class->event = ec_event; + class->focus = ec_focus; + class->unfocus = ec_unfocus; + class->height = ec_height; + class->enter_edit = ec_enter_edit; + class->leave_edit = ec_leave_edit; + class->save_state = ec_save_state; + class->load_state = ec_load_state; + class->free_state = ec_free_state; + class->print = NULL; + class->print_height = NULL; + class->max_width = NULL; + class->max_width_by_row = NULL; +} + +static void +e_cell_init (ECell *cell) +{ +} + +/** + * e_cell_event: + * @ecell_view: The ECellView where the event will be dispatched + * @event: The GdkEvent. + * @model_col: the column in the model + * @view_col: the column in the view + * @row: the row + * @flags: flags about the current state + * @actions: a second return value in case the cell wants to take some action + * (specifically grabbing & ungrabbing) + * + * Dispatches the event @event to the @ecell_view for. + * + * Returns: processing state from the GdkEvent handling. + */ +gint +e_cell_event (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + return class->event ( + ecell_view, event, model_col, + view_col, row, flags, actions); +} + +/** + * e_cell_new_view: + * @ecell: the Ecell that will create the new view + * @table_model: the table model the ecell is bound to + * @e_table_item_view: an ETableItem object (the CanvasItem that + * reprensents the view of the table) + * + * ECell renderers new to be bound to a table_model and to the actual view + * during their life time to actually render the data. This method is invoked + * by the ETableItem canvas item to instatiate a new view of the ECell. + * + * This is invoked when the ETableModel is attached to the ETableItem + * (a CanvasItem that can render ETableModels in the screen). + * + * Returns: a new ECellView for this @ecell on the @table_model displayed + * on the @e_table_item_view. + */ +ECellView * +e_cell_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view) +{ + return E_CELL_GET_CLASS (ecell)->new_view ( + ecell, table_model, e_table_item_view); +} + +/** + * e_cell_realize: + * @ecell_view: The ECellView to be realized. + * + * This function is invoked to give a chance to the ECellView to allocate + * any resources it needs from Gdk, equivalent to the GtkWidget::realize + * signal. + */ +void +e_cell_realize (ECellView *ecell_view) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + g_return_if_fail (class->realize != NULL); + + class->realize (ecell_view); +} + +/** + * e_cell_kill_view: + * @ecell_view: view to be destroyed. + * + * This method it used to destroy a view of an ECell renderer + */ +void +e_cell_kill_view (ECellView *ecell_view) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + g_return_if_fail (class->kill_view != NULL); + + class->kill_view (ecell_view); +} + +/** + * e_cell_unrealize: + * @ecell_view: The ECellView to be unrealized. + * + * This function is invoked to give a chance to the ECellView to + * release any resources it allocated during the realize method, + * equivalent to the GtkWidget::unrealize signal. + */ +void +e_cell_unrealize (ECellView *ecell_view) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + g_return_if_fail (class->unrealize != NULL); + + class->unrealize (ecell_view); +} + +/** + * e_cell_draw: + * @ecell_view: the ECellView to redraw + * @cr: a Cairo context + * @model_col: the column in the model being drawn. + * @view_col: the column in the view being drawn (what the model maps to). + * @row: the row being drawn + * @flags: rendering flags. + * @x1: boudary for the rendering + * @y1: boudary for the rendering + * @x2: boudary for the rendering + * @y2: boudary for the rendering + * + * This instructs the ECellView to render itself into the Cairo context. + * The region to be drawn in given by (x1,y1)-(x2,y2). + * + * The most important flags are %E_CELL_SELECTED and %E_CELL_FOCUSED, other + * flags include alignments and justifications. + */ +void +e_cell_draw (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2) +{ + ECellClass *class; + + g_return_if_fail (ecell_view != NULL); + g_return_if_fail (row >= 0); + g_return_if_fail (row < e_table_model_row_count (ecell_view->e_table_model)); + + class = E_CELL_GET_CLASS (ecell_view->ecell); + g_return_if_fail (class->draw != NULL); + + cairo_save (cr); + + class->draw ( + ecell_view, cr, + model_col, view_col, + row, flags, x1, y1, x2, y2); + + cairo_restore (cr); +} + +/** + * e_cell_print: + * @ecell_view: the ECellView to redraw + * @context: The GtkPrintContext where we output our printed data. + * @model_col: the column in the model being drawn. + * @view_col: the column in the view being drawn (what the model maps to). + * @row: the row being drawn + * @width: width + * @height: height + * + * FIXME: + */ +void +e_cell_print (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + if (class->print != NULL) + class->print ( + ecell_view, context, + model_col, view_col, + row, width, height); +} + +/** + * e_cell_print: + * + * FIXME: + */ +gdouble +e_cell_print_height (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + if (class->print_height == NULL) + return 0.0; + + return class->print_height ( + ecell_view, context, + model_col, view_col, + row, width); +} + +/** + * e_cell_height: + * @ecell_view: the ECellView. + * @model_col: the column in the model + * @view_col: the column in the view. + * @row: the row to me measured + * + * Returns: the height of the cell at @model_col, @row rendered at + * @view_col, @row. + */ +gint +e_cell_height (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + g_return_val_if_fail (class->height != NULL, 0); + + return class->height (ecell_view, model_col, view_col, row); +} + +/** + * e_cell_enter_edit: + * @ecell_view: the ECellView that will enter editing + * @model_col: the column in the model + * @view_col: the column in the view + * @row: the row + * + * Notifies the ECellView that it is about to enter editing mode for + * @model_col, @row rendered at @view_col, @row. + */ +gpointer +e_cell_enter_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + g_return_val_if_fail (class->enter_edit != NULL, NULL); + + return class->enter_edit (ecell_view, model_col, view_col, row); +} + +/** + * e_cell_leave_edit: + * @ecell_view: the ECellView that will leave editing + * @model_col: the column in the model + * @view_col: the column in the view + * @row: the row + * @edit_context: the editing context + * + * Notifies the ECellView that editing is finished at @model_col, @row + * rendered at @view_col, @row. + */ +void +e_cell_leave_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + g_return_if_fail (class->leave_edit != NULL); + + class->leave_edit (ecell_view, model_col, view_col, row, edit_context); +} + +/** + * e_cell_save_state: + * @ecell_view: the ECellView to save + * @model_col: the column in the model + * @view_col: the column in the view + * @row: the row + * @edit_context: the editing context + * + * Returns: The save state. + * + * Requests that the ECellView return a gpointer representing the state + * of the ECell. This is primarily intended for things like selection + * or scrolling. + */ +gpointer +e_cell_save_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + if (class->save_state == NULL) + return NULL; + + return class->save_state ( + ecell_view, model_col, view_col, row, edit_context); +} + +/** + * e_cell_load_state: + * @ecell_view: the ECellView to load + * @model_col: the column in the model + * @view_col: the column in the view + * @row: the row + * @edit_context: the editing context + * @save_state: the save state to load from + * + * Requests that the ECellView load from the given save state. + */ +void +e_cell_load_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context, + gpointer save_state) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + if (class->load_state != NULL) + class->load_state ( + ecell_view, model_col, view_col, + row, edit_context, save_state); +} + +/** + * e_cell_free_state: + * @ecell_view: the ECellView + * @model_col: the column in the model + * @view_col: the column in the view + * @row: the row + * @edit_context: the editing context + * @save_state: the save state to free + * + * Requests that the ECellView free the given save state. + */ +void +e_cell_free_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer save_state) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + if (class->free_state != NULL) + class->free_state ( + ecell_view, model_col, view_col, row, save_state); +} + +/** + * e_cell_max_width: + * @ecell_view: the ECellView that will leave editing + * @model_col: the column in the model + * @view_col: the column in the view. + * + * Returns: the maximum width for the ECellview at @model_col which + * is being rendered as @view_col + */ +gint +e_cell_max_width (ECellView *ecell_view, + gint model_col, + gint view_col) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + g_return_val_if_fail (class->max_width != NULL, 0); + + return class->max_width (ecell_view, model_col, view_col); +} + +/** + * e_cell_max_width_by_row: + * @ecell_view: the ECellView that we are curious about + * @model_col: the column in the model + * @view_col: the column in the view. + * @row: The row in the model. + * + * Returns: the maximum width for the ECellview at @model_col which + * is being rendered as @view_col for the data in @row. + */ +gint +e_cell_max_width_by_row (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + if (class->max_width_by_row == NULL) + return e_cell_max_width (ecell_view, model_col, view_col); + + return class->max_width_by_row (ecell_view, model_col, view_col, row); +} + +/** + * e_cell_max_width_by_row_implemented: + * @ecell_view: the ECellView that we are curious about + * @model_col: the column in the model + * @view_col: the column in the view. + * @row: The row in the model. + * + * Returns: the maximum width for the ECellview at @model_col which + * is being rendered as @view_col for the data in @row. + */ +gboolean +e_cell_max_width_by_row_implemented (ECellView *ecell_view) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + return (class->max_width_by_row != NULL); +} + +gchar * +e_cell_get_bg_color (ECellView *ecell_view, + gint row) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + if (class->get_bg_color == NULL) + return NULL; + + return class->get_bg_color (ecell_view, row); +} + +void +e_cell_style_set (ECellView *ecell_view, + GtkStyle *previous_style) +{ + ECellClass *class; + + class = E_CELL_GET_CLASS (ecell_view->ecell); + + if (class->style_set != NULL) + class->style_set (ecell_view, previous_style); +} + diff --git a/e-util/e-cell.h b/e-util/e-cell.h new file mode 100644 index 0000000000..4c1354259c --- /dev/null +++ b/e-util/e-cell.h @@ -0,0 +1,299 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_CELL_H_ +#define _E_CELL_H_ + +#include <gtk/gtk.h> + +#include <e-util/e-table-model.h> + +/* Standard GObject macros */ +#define E_TYPE_CELL \ + (e_cell_get_type ()) +#define E_CELL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CELL, ECell)) +#define E_CELL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CELL, ECellClass)) +#define E_CELL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL, ECellClass)) +#define E_IS_CELL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CELL)) +#define E_IS_CELL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CELL)) +#define E_CELL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CELL, ECellClass)) + +G_BEGIN_DECLS + +typedef struct _ECell ECell; +typedef struct _ECellClass ECellClass; +typedef struct _ECellView ECellView; + +typedef gboolean (*ETableSearchFunc) (gconstpointer haystack, + const gchar *needle); + +typedef enum { + E_CELL_SELECTED = 1 << 0, + + E_CELL_JUSTIFICATION = 3 << 1, + E_CELL_JUSTIFY_CENTER = 0 << 1, + E_CELL_JUSTIFY_LEFT = 1 << 1, + E_CELL_JUSTIFY_RIGHT = 2 << 1, + E_CELL_JUSTIFY_FILL = 3 << 1, + + E_CELL_ALIGN_LEFT = 1 << 1, + E_CELL_ALIGN_RIGHT = 1 << 2, + + E_CELL_FOCUSED = 1 << 3, + + E_CELL_EDITING = 1 << 4, + + E_CELL_CURSOR = 1 << 5, + + E_CELL_PREEDIT = 1 << 6 +} ECellFlags; + +typedef enum { + E_CELL_GRAB = 1 << 0, + E_CELL_UNGRAB = 1 << 1 +} ECellActions; + +struct _ECellView { + ECell *ecell; + ETableModel *e_table_model; + void *e_table_item_view; + + gint focus_x1, focus_y1, focus_x2, focus_y2; + gint focus_col, focus_row; + + void (*kill_view_cb) (struct _ECellView *, gpointer); + GList *kill_view_cb_data; +}; + +#define E_CELL_IS_FOCUSED(ecell_view) (ecell_view->focus_x1 != -1) + +struct _ECell { + GObject parent; +}; + +struct _ECellClass { + GObjectClass parent_class; + + ECellView * (*new_view) (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view); + void (*kill_view) (ECellView *ecell_view); + + void (*realize) (ECellView *ecell_view); + void (*unrealize) (ECellView *ecell_view); + + void (*draw) (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2); + gint (*event) (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions); + void (*focus) (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gint x1, + gint y1, + gint x2, + gint y2); + void (*unfocus) (ECellView *ecell_view); + gint (*height) (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row); + + gpointer (*enter_edit) (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row); + void (*leave_edit) (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer context); + gpointer (*save_state) (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer context); + void (*load_state) (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer context, + gpointer save_state); + void (*free_state) (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer save_state); + void (*print) (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height); + gdouble (*print_height) (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width); + gint (*max_width) (ECellView *ecell_view, + gint model_col, + gint view_col); + gint (*max_width_by_row) (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row); + gchar * (*get_bg_color) (ECellView *ecell_view, + gint row); + + void (*style_set) (ECellView *ecell_view, + GtkStyle *previous_style); +}; + +GType e_cell_get_type (void) G_GNUC_CONST; + +/* View creation methods. */ +ECellView * e_cell_new_view (ECell *ecell, + ETableModel *table_model, + gpointer e_table_item_view); +void e_cell_kill_view (ECellView *ecell_view); + +/* Cell View methods. */ +gint e_cell_event (ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + ECellActions *actions); +void e_cell_realize (ECellView *ecell_view); +void e_cell_unrealize (ECellView *ecell_view); +void e_cell_draw (ECellView *ecell_view, + cairo_t *cr, + gint model_col, + gint view_col, + gint row, + ECellFlags flags, + gint x1, + gint y1, + gint x2, + gint y2); +void e_cell_print (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width, + gdouble height); +gdouble e_cell_print_height (ECellView *ecell_view, + GtkPrintContext *context, + gint model_col, + gint view_col, + gint row, + gdouble width); +gint e_cell_max_width (ECellView *ecell_view, + gint model_col, + gint view_col); +gint e_cell_max_width_by_row (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row); +gboolean e_cell_max_width_by_row_implemented + (ECellView *ecell_view); +gchar * e_cell_get_bg_color (ECellView *ecell_view, + gint row); +void e_cell_style_set (ECellView *ecell_view, + GtkStyle *previous_style); + +void e_cell_focus (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gint x1, + gint y1, + gint x2, + gint y2); +void e_cell_unfocus (ECellView *ecell_view); +gint e_cell_height (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row); +gpointer e_cell_enter_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row); +void e_cell_leave_edit (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context); +gpointer e_cell_save_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context); +void e_cell_load_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer edit_context, + gpointer state); +void e_cell_free_state (ECellView *ecell_view, + gint model_col, + gint view_col, + gint row, + gpointer state); + +G_END_DECLS + +#endif /* _E_CELL_H_ */ diff --git a/e-util/e-charset-combo-box.c b/e-util/e-charset-combo-box.c new file mode 100644 index 0000000000..1423a592d8 --- /dev/null +++ b/e-util/e-charset-combo-box.c @@ -0,0 +1,407 @@ +/* + * e-charset-combo-box.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-charset-combo-box.h" + +#include <glib/gi18n.h> + +#include "e-charset.h" +#include "e-misc-utils.h" + +#define E_CHARSET_COMBO_BOX_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CHARSET_COMBO_BOX, ECharsetComboBoxPrivate)) + +#define DEFAULT_CHARSET "UTF-8" +#define OTHER_VALUE G_MAXINT + +struct _ECharsetComboBoxPrivate { + GtkActionGroup *action_group; + GtkRadioAction *other_action; + GHashTable *charset_index; + + /* Used when the user clicks Cancel in the character set + * dialog. Reverts to the previous combo box setting. */ + gint previous_index; + + /* When setting the character set programmatically, this + * prevents the custom character set dialog from running. */ + guint block_dialog : 1; +}; + +enum { + PROP_0, + PROP_CHARSET +}; + +G_DEFINE_TYPE ( + ECharsetComboBox, + e_charset_combo_box, + E_TYPE_ACTION_COMBO_BOX) + +static void +charset_combo_box_entry_changed_cb (GtkEntry *entry, + GtkDialog *dialog) +{ + const gchar *text; + gboolean sensitive; + + text = gtk_entry_get_text (entry); + sensitive = (text != NULL && *text != '\0'); + gtk_dialog_set_response_sensitive (dialog, GTK_RESPONSE_OK, sensitive); +} + +static void +charset_combo_box_run_dialog (ECharsetComboBox *combo_box) +{ + GtkDialog *dialog; + GtkEntry *entry; + GtkWidget *container; + GtkWidget *widget; + GObject *object; + gpointer parent; + const gchar *charset; + + /* FIXME Using a dialog for this is lame. Selecting "Other..." + * should unlock an entry directly in the Preferences tab. + * Unfortunately space in Preferences is at a premium right + * now, but we should revisit this when the space issue is + * finally resolved. */ + + parent = gtk_widget_get_toplevel (GTK_WIDGET (combo_box)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + object = G_OBJECT (combo_box->priv->other_action); + charset = g_object_get_data (object, "charset"); + + widget = gtk_dialog_new_with_buttons ( + _("Character Encoding"), parent, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, NULL); + + /* Load the broken border width defaults so we can override them. */ + gtk_widget_ensure_style (widget); + + dialog = GTK_DIALOG (widget); + + gtk_dialog_set_default_response (dialog, GTK_RESPONSE_OK); + + gtk_container_set_border_width (GTK_CONTAINER (dialog), 12); + + widget = gtk_dialog_get_action_area (dialog); + gtk_container_set_border_width (GTK_CONTAINER (widget), 0); + + widget = gtk_dialog_get_content_area (dialog); + gtk_box_set_spacing (GTK_BOX (widget), 12); + gtk_container_set_border_width (GTK_CONTAINER (widget), 0); + + container = widget; + + widget = gtk_label_new (_("Enter the character set to use")); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_alignment_new (0.0, 0.0, 1.0, 1.0); + gtk_alignment_set_padding (GTK_ALIGNMENT (widget), 0, 0, 12, 0); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_entry_new (); + entry = GTK_ENTRY (widget); + gtk_entry_set_activates_default (entry, TRUE); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + g_signal_connect ( + entry, "changed", + G_CALLBACK (charset_combo_box_entry_changed_cb), dialog); + + /* Set the default text -after- connecting the signal handler. + * This will initialize the "OK" button to the proper state. */ + gtk_entry_set_text (entry, charset); + + if (gtk_dialog_run (dialog) != GTK_RESPONSE_OK) { + gint active; + + /* Revert to the previously selected character set. */ + combo_box->priv->block_dialog = TRUE; + active = combo_box->priv->previous_index; + gtk_combo_box_set_active (GTK_COMBO_BOX (combo_box), active); + combo_box->priv->block_dialog = FALSE; + + goto exit; + } + + charset = gtk_entry_get_text (entry); + g_return_if_fail (charset != NULL && charset != '\0'); + + g_object_set_data_full ( + object, "charset", g_strdup (charset), + (GDestroyNotify) g_free); + +exit: + gtk_widget_destroy (GTK_WIDGET (dialog)); +} + +static void +charset_combo_box_notify_charset_cb (ECharsetComboBox *combo_box) +{ + GtkToggleAction *action; + + action = GTK_TOGGLE_ACTION (combo_box->priv->other_action); + if (!gtk_toggle_action_get_active (action)) + return; + + if (combo_box->priv->block_dialog) + return; + + /* "Other" action was selected by user. */ + charset_combo_box_run_dialog (combo_box); +} + +static void +charset_combo_box_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CHARSET: + e_charset_combo_box_set_charset ( + E_CHARSET_COMBO_BOX (object), + g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +charset_combo_box_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CHARSET: + g_value_set_string ( + value, e_charset_combo_box_get_charset ( + E_CHARSET_COMBO_BOX (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +charset_combo_box_dispose (GObject *object) +{ + ECharsetComboBoxPrivate *priv; + + priv = E_CHARSET_COMBO_BOX_GET_PRIVATE (object); + + if (priv->action_group != NULL) { + g_object_unref (priv->action_group); + priv->action_group = NULL; + } + + if (priv->other_action != NULL) { + g_object_unref (priv->other_action); + priv->other_action = NULL; + } + + g_hash_table_remove_all (priv->charset_index); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_charset_combo_box_parent_class)->dispose (object); +} + +static void +charset_combo_box_finalize (GObject *object) +{ + ECharsetComboBoxPrivate *priv; + + priv = E_CHARSET_COMBO_BOX_GET_PRIVATE (object); + + g_hash_table_destroy (priv->charset_index); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_charset_combo_box_parent_class)->finalize (object); +} + +static void +charset_combo_box_changed (GtkComboBox *combo_box) +{ + ECharsetComboBoxPrivate *priv; + + priv = E_CHARSET_COMBO_BOX_GET_PRIVATE (combo_box); + + /* Chain up to parent's changed() method. */ + GTK_COMBO_BOX_CLASS (e_charset_combo_box_parent_class)-> + changed (combo_box); + + /* Notify -before- updating previous index. */ + g_object_notify (G_OBJECT (combo_box), "charset"); + priv->previous_index = gtk_combo_box_get_active (combo_box); +} + +static void +e_charset_combo_box_class_init (ECharsetComboBoxClass *class) +{ + GObjectClass *object_class; + GtkComboBoxClass *combo_box_class; + + g_type_class_add_private (class, sizeof (ECharsetComboBoxPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = charset_combo_box_set_property; + object_class->get_property = charset_combo_box_get_property; + object_class->dispose = charset_combo_box_dispose; + object_class->finalize = charset_combo_box_finalize; + + combo_box_class = GTK_COMBO_BOX_CLASS (class); + combo_box_class->changed = charset_combo_box_changed; + + g_object_class_install_property ( + object_class, + PROP_CHARSET, + g_param_spec_string ( + "charset", + "Charset", + "The selected character set", + "UTF-8", + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); +} + +static void +e_charset_combo_box_init (ECharsetComboBox *combo_box) +{ + GtkActionGroup *action_group; + GtkRadioAction *radio_action; + GHashTable *charset_index; + GSList *group, *iter; + + action_group = gtk_action_group_new ("charset-combo-box-internal"); + + charset_index = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_object_unref); + + combo_box->priv = E_CHARSET_COMBO_BOX_GET_PRIVATE (combo_box); + combo_box->priv->action_group = action_group; + combo_box->priv->charset_index = charset_index; + + group = e_charset_add_radio_actions ( + action_group, "charset-", NULL, NULL, NULL); + + /* Populate the character set index. */ + for (iter = group; iter != NULL; iter = iter->next) { + GObject *object = iter->data; + const gchar *charset; + + charset = g_object_get_data (object, "charset"); + g_return_if_fail (charset != NULL); + + g_hash_table_insert ( + charset_index, g_strdup (charset), + g_object_ref (object)); + } + + /* Note the "other" action is not included in the index. */ + + radio_action = gtk_radio_action_new ( + "charset-other", _("Other..."), NULL, NULL, OTHER_VALUE); + + g_object_set_data (G_OBJECT (radio_action), "charset", (gpointer) ""); + + gtk_radio_action_set_group (radio_action, group); + group = gtk_radio_action_get_group (radio_action); + + e_action_combo_box_set_action ( + E_ACTION_COMBO_BOX (combo_box), radio_action); + + e_action_combo_box_add_separator_after ( + E_ACTION_COMBO_BOX (combo_box), g_slist_length (group)); + + g_signal_connect ( + combo_box, "notify::charset", + G_CALLBACK (charset_combo_box_notify_charset_cb), NULL); + + combo_box->priv->other_action = radio_action; +} + +GtkWidget * +e_charset_combo_box_new (void) +{ + return g_object_new (E_TYPE_CHARSET_COMBO_BOX, NULL); +} + +const gchar * +e_charset_combo_box_get_charset (ECharsetComboBox *combo_box) +{ + GtkRadioAction *radio_action; + + g_return_val_if_fail (E_IS_CHARSET_COMBO_BOX (combo_box), NULL); + + radio_action = combo_box->priv->other_action; + radio_action = e_radio_action_get_current_action (radio_action); + + return g_object_get_data (G_OBJECT (radio_action), "charset"); +} + +void +e_charset_combo_box_set_charset (ECharsetComboBox *combo_box, + const gchar *charset) +{ + GHashTable *charset_index; + GtkRadioAction *radio_action; + + g_return_if_fail (E_IS_CHARSET_COMBO_BOX (combo_box)); + + if (charset == NULL || *charset == '\0') + charset = "UTF-8"; + + charset_index = combo_box->priv->charset_index; + radio_action = g_hash_table_lookup (charset_index, charset); + + if (radio_action == NULL) { + radio_action = combo_box->priv->other_action; + g_object_set_data_full ( + G_OBJECT (radio_action), + "charset", g_strdup (charset), + (GDestroyNotify) g_free); + } + + combo_box->priv->block_dialog = TRUE; + gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (radio_action), TRUE); + combo_box->priv->block_dialog = FALSE; +} diff --git a/e-util/e-charset-combo-box.h b/e-util/e-charset-combo-box.h new file mode 100644 index 0000000000..54c5527875 --- /dev/null +++ b/e-util/e-charset-combo-box.h @@ -0,0 +1,73 @@ +/* + * e-charset-combo-box.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CHARSET_COMBO_BOX_H +#define E_CHARSET_COMBO_BOX_H + +#include <e-util/e-action-combo-box.h> + +/* Standard GObject macros */ +#define E_TYPE_CHARSET_COMBO_BOX \ + (e_charset_combo_box_get_type ()) +#define E_CHARSET_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CHARSET_COMBO_BOX, ECharsetComboBox)) +#define E_CHARSET_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CHARSET_COMBO_BOX, ECharsetComboBoxClass)) +#define E_IS_CHARSET_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CHARSET_COMBO_BOX)) +#define E_IS_CHARSET_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CHARSET_COMBO_BOX)) +#define E_CHARSET_COMBO_BOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CHARSET_COMBO_BOX, ECharsetComboBoxClass)) + +G_BEGIN_DECLS + +typedef struct _ECharsetComboBox ECharsetComboBox; +typedef struct _ECharsetComboBoxClass ECharsetComboBoxClass; +typedef struct _ECharsetComboBoxPrivate ECharsetComboBoxPrivate; + +struct _ECharsetComboBox { + EActionComboBox parent; + ECharsetComboBoxPrivate *priv; +}; + +struct _ECharsetComboBoxClass { + EActionComboBoxClass parent_class; +}; + +GType e_charset_combo_box_get_type (void); +GtkWidget * e_charset_combo_box_new (void); +const gchar * e_charset_combo_box_get_charset (ECharsetComboBox *combo_box); +void e_charset_combo_box_set_charset (ECharsetComboBox *combo_box, + const gchar *charset); + +G_END_DECLS + +#endif /* E_CHARSET_COMBO_BOX_H */ diff --git a/e-util/e-charset.h b/e-util/e-charset.h index 57b6976a1f..29bdc508c6 100644 --- a/e-util/e-charset.h +++ b/e-util/e-charset.h @@ -18,6 +18,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_CHARSET_H #define E_CHARSET_H diff --git a/e-util/e-client-utils.c b/e-util/e-client-utils.c new file mode 100644 index 0000000000..ed0688b637 --- /dev/null +++ b/e-util/e-client-utils.c @@ -0,0 +1,445 @@ +/* + * e-client-utils.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 2011 Red Hat, Inc. (www.redhat.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <glib/gi18n-lib.h> +#include <gtk/gtk.h> +#include <libsoup/soup.h> + +#include <libebook/libebook.h> +#include <libecal/libecal.h> + +#include "e-client-utils.h" + +/** + * e_client_utils_new: + * + * Proxy function for e_book_client_utils_new() and e_cal_client_utils_new(). + * + * Since: 3.2 + **/ +EClient * +e_client_utils_new (ESource *source, + EClientSourceType source_type, + GError **error) +{ + EClient *res = NULL; + + g_return_val_if_fail (source != NULL, NULL); + g_return_val_if_fail (E_IS_SOURCE (source), NULL); + + switch (source_type) { + case E_CLIENT_SOURCE_TYPE_CONTACTS: + res = E_CLIENT (e_book_client_new (source, error)); + break; + case E_CLIENT_SOURCE_TYPE_EVENTS: + res = E_CLIENT (e_cal_client_new (source, E_CAL_CLIENT_SOURCE_TYPE_EVENTS, error)); + break; + case E_CLIENT_SOURCE_TYPE_MEMOS: + res = E_CLIENT (e_cal_client_new (source, E_CAL_CLIENT_SOURCE_TYPE_MEMOS, error)); + break; + case E_CLIENT_SOURCE_TYPE_TASKS: + res = E_CLIENT (e_cal_client_new (source, E_CAL_CLIENT_SOURCE_TYPE_TASKS, error)); + break; + default: + g_return_val_if_reached (NULL); + break; + } + + return res; +} + +typedef struct _EClientUtilsAsyncOpData { + GAsyncReadyCallback callback; + gpointer user_data; + GCancellable *cancellable; + ESource *source; + EClient *client; + gboolean open_finished; + GError *opened_cb_error; + guint retry_open_id; + gboolean only_if_exists; + guint pending_properties_count; +} EClientUtilsAsyncOpData; + +static void +free_client_utils_async_op_data (EClientUtilsAsyncOpData *async_data) +{ + g_return_if_fail (async_data != NULL); + g_return_if_fail (async_data->cancellable != NULL); + g_return_if_fail (async_data->client != NULL); + + if (async_data->retry_open_id) + g_source_remove (async_data->retry_open_id); + + g_signal_handlers_disconnect_matched (async_data->cancellable, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, async_data); + g_signal_handlers_disconnect_matched (async_data->client, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, async_data); + + if (async_data->opened_cb_error) + g_error_free (async_data->opened_cb_error); + g_object_unref (async_data->cancellable); + g_object_unref (async_data->client); + g_object_unref (async_data->source); + g_free (async_data); +} + +static gboolean +complete_async_op_in_idle_cb (gpointer user_data) +{ + GSimpleAsyncResult *simple = user_data; + gint run_main_depth; + + g_return_val_if_fail (simple != NULL, FALSE); + + run_main_depth = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (simple), "run-main-depth")); + if (run_main_depth < 1) + run_main_depth = 1; + + /* do not receive in higher level than was initially run */ + if (g_main_depth () > run_main_depth) { + return TRUE; + } + + g_simple_async_result_complete (simple); + g_object_unref (simple); + + return FALSE; +} + +#define return_async_error_if_fail(expr, callback, user_data, src, source_tag) G_STMT_START { \ + if (G_LIKELY ((expr))) { } else { \ + GError *error; \ + \ + error = g_error_new (E_CLIENT_ERROR, E_CLIENT_ERROR_INVALID_ARG, \ + "%s: assertion '%s' failed", G_STRFUNC, #expr); \ + \ + return_async_error (error, callback, user_data, src, source_tag); \ + g_error_free (error); \ + return; \ + } \ + } G_STMT_END + +static void +return_async_error (const GError *error, + GAsyncReadyCallback callback, + gpointer user_data, + ESource *source, + gpointer source_tag) +{ + GSimpleAsyncResult *simple; + + g_return_if_fail (error != NULL); + g_return_if_fail (source_tag != NULL); + + simple = g_simple_async_result_new (G_OBJECT (source), callback, user_data, source_tag); + g_simple_async_result_set_from_error (simple, error); + + g_object_set_data (G_OBJECT (simple), "run-main-depth", GINT_TO_POINTER (g_main_depth ())); + g_idle_add (complete_async_op_in_idle_cb, simple); +} + +static void +client_utils_get_backend_property_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EClient *client = E_CLIENT (source_object); + EClientUtilsAsyncOpData *async_data = user_data; + GSimpleAsyncResult *simple; + + g_return_if_fail (async_data != NULL); + g_return_if_fail (async_data->client != NULL); + g_return_if_fail (async_data->client == client); + + if (result) { + gchar *prop_value = NULL; + + if (e_client_get_backend_property_finish (client, result, &prop_value, NULL)) + g_free (prop_value); + + async_data->pending_properties_count--; + if (async_data->pending_properties_count) + return; + } + + simple = g_simple_async_result_new (G_OBJECT (async_data->source), async_data->callback, async_data->user_data, e_client_utils_open_new); + g_simple_async_result_set_op_res_gpointer (simple, g_object_ref (async_data->client), g_object_unref); + + g_object_set_data (G_OBJECT (simple), "run-main-depth", GINT_TO_POINTER (g_main_depth ())); + g_idle_add (complete_async_op_in_idle_cb, simple); + + free_client_utils_async_op_data (async_data); +} + +static void +client_utils_capabilities_retrieved_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EClient *client = E_CLIENT (source_object); + EClientUtilsAsyncOpData *async_data = user_data; + gchar *capabilities = NULL; + gboolean caps_res; + + g_return_if_fail (async_data != NULL); + g_return_if_fail (async_data->client != NULL); + g_return_if_fail (async_data->client == client); + + caps_res = e_client_retrieve_capabilities_finish (client, result, &capabilities, NULL); + g_free (capabilities); + + if (caps_res) { + async_data->pending_properties_count = 1; + + /* precache backend properties */ + if (E_IS_CAL_CLIENT (client)) { + async_data->pending_properties_count += 3; + + e_client_get_backend_property (async_data->client, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS, async_data->cancellable, client_utils_get_backend_property_cb, async_data); + e_client_get_backend_property (async_data->client, CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS, async_data->cancellable, client_utils_get_backend_property_cb, async_data); + e_client_get_backend_property (async_data->client, CAL_BACKEND_PROPERTY_DEFAULT_OBJECT, async_data->cancellable, client_utils_get_backend_property_cb, async_data); + } else if (E_IS_BOOK_CLIENT (client)) { + async_data->pending_properties_count += 2; + + e_client_get_backend_property (async_data->client, BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS, async_data->cancellable, client_utils_get_backend_property_cb, async_data); + e_client_get_backend_property (async_data->client, BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS, async_data->cancellable, client_utils_get_backend_property_cb, async_data); + } else { + g_warn_if_reached (); + client_utils_get_backend_property_cb (source_object, NULL, async_data); + return; + } + + e_client_get_backend_property (async_data->client, CLIENT_BACKEND_PROPERTY_CACHE_DIR, async_data->cancellable, client_utils_get_backend_property_cb, async_data); + } else { + client_utils_get_backend_property_cb (source_object, NULL, async_data); + } +} + +static void +client_utils_open_new_done (EClientUtilsAsyncOpData *async_data) +{ + g_return_if_fail (async_data != NULL); + g_return_if_fail (async_data->client != NULL); + + /* retrieve capabilities just to have them cached on #EClient for later use */ + e_client_retrieve_capabilities (async_data->client, async_data->cancellable, client_utils_capabilities_retrieved_cb, async_data); +} + +static gboolean client_utils_retry_open_timeout_cb (gpointer user_data); +static void client_utils_opened_cb (EClient *client, const GError *error, EClientUtilsAsyncOpData *async_data); + +static void +finish_or_retry_open (EClientUtilsAsyncOpData *async_data, + const GError *error) +{ + g_return_if_fail (async_data != NULL); + + if (error && g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_BUSY)) { + /* postpone for 1/2 of a second, backend is busy now */ + async_data->open_finished = FALSE; + async_data->retry_open_id = g_timeout_add (500, client_utils_retry_open_timeout_cb, async_data); + } else if (error) { + return_async_error (error, async_data->callback, async_data->user_data, async_data->source, e_client_utils_open_new); + free_client_utils_async_op_data (async_data); + } else { + client_utils_open_new_done (async_data); + } +} + +static void +client_utils_opened_cb (EClient *client, + const GError *error, + EClientUtilsAsyncOpData *async_data) +{ + g_return_if_fail (client != NULL); + g_return_if_fail (async_data != NULL); + g_return_if_fail (client == async_data->client); + + g_signal_handlers_disconnect_by_func (client, G_CALLBACK (client_utils_opened_cb), async_data); + + if (!async_data->open_finished) { + /* there can happen that the "opened" signal is received + * before the e_client_open () is finished, thus keep detailed + * error for later use, if any */ + if (error) + async_data->opened_cb_error = g_error_copy (error); + } else { + finish_or_retry_open (async_data, error); + } +} + +static void +client_utils_open_new_async_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EClientUtilsAsyncOpData *async_data = user_data; + GError *error = NULL; + + g_return_if_fail (source_object != NULL); + g_return_if_fail (result != NULL); + g_return_if_fail (async_data != NULL); + g_return_if_fail (async_data->callback != NULL); + g_return_if_fail (async_data->client == E_CLIENT (source_object)); + + async_data->open_finished = TRUE; + + if (!e_client_open_finish (E_CLIENT (source_object), result, &error) + || g_cancellable_set_error_if_cancelled (async_data->cancellable, &error)) { + finish_or_retry_open (async_data, error); + g_error_free (error); + return; + } + + if (async_data->opened_cb_error) { + finish_or_retry_open (async_data, async_data->opened_cb_error); + return; + } + + if (e_client_is_opened (async_data->client)) { + client_utils_open_new_done (async_data); + return; + } + + /* wait for 'opened' signal, which is received in client_utils_opened_cb */ +} + +static gboolean +client_utils_retry_open_timeout_cb (gpointer user_data) +{ + EClientUtilsAsyncOpData *async_data = user_data; + + g_return_val_if_fail (async_data != NULL, FALSE); + + g_signal_handlers_disconnect_matched (async_data->cancellable, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, async_data); + + /* reconnect to the signal */ + g_signal_connect (async_data->client, "opened", G_CALLBACK (client_utils_opened_cb), async_data); + + e_client_open (async_data->client, async_data->only_if_exists, async_data->cancellable, client_utils_open_new_async_cb, async_data); + + async_data->retry_open_id = 0; + + return FALSE; +} + +/** + * e_client_utils_open_new: + * @source: an #ESource to be opened + * @source_type: an #EClientSourceType of the @source + * @only_if_exists: if %TRUE, fail if this client doesn't already exist, otherwise create it first + * @cancellable: a #GCancellable; can be %NULL + * @callback: callback to call when a result is ready + * @user_data: user data for the @callback + * + * Begins asynchronous opening of a new #EClient corresponding + * to the @source of type @source_type. The resulting #EClient + * is fully opened and authenticated client, ready to be used. + * The opened client has also fetched capabilities. + * This call is finished by e_client_utils_open_new_finish() + * from the @callback. + * + * Since: 3.2 + **/ +void +e_client_utils_open_new (ESource *source, + EClientSourceType source_type, + gboolean only_if_exists, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + EClient *client; + GError *error = NULL; + EClientUtilsAsyncOpData *async_data; + + g_return_if_fail (callback != NULL); + return_async_error_if_fail (source != NULL, callback, user_data, source, e_client_utils_open_new); + return_async_error_if_fail (E_IS_SOURCE (source), callback, user_data, source, e_client_utils_open_new); + + client = e_client_utils_new (source, source_type, &error); + if (!client) { + return_async_error (error, callback, user_data, source, e_client_utils_open_new); + g_error_free (error); + return; + } + + async_data = g_new0 (EClientUtilsAsyncOpData, 1); + async_data->callback = callback; + async_data->user_data = user_data; + async_data->source = g_object_ref (source); + async_data->client = client; + async_data->open_finished = FALSE; + async_data->only_if_exists = only_if_exists; + async_data->retry_open_id = 0; + + if (cancellable) + async_data->cancellable = g_object_ref (cancellable); + else + async_data->cancellable = g_cancellable_new (); + + /* wait till backend notifies about its opened state */ + g_signal_connect (client, "opened", G_CALLBACK (client_utils_opened_cb), async_data); + + e_client_open (async_data->client, async_data->only_if_exists, async_data->cancellable, client_utils_open_new_async_cb, async_data); +} + +/** + * e_client_utils_open_new_finish: + * @source: an #ESource on which the e_client_utils_open_new() was invoked + * @result: a #GAsyncResult + * @client: (out): Return value for an #EClient + * @error: (out): a #GError to set an error, if any + * + * Finishes previous call of e_client_utils_open_new() and + * sets @client to a fully opened and authenticated #EClient. + * This @client, if not NULL, should be freed with g_object_unref(). + * + * Returns: %TRUE if successful, %FALSE otherwise. + * + * Since: 3.2 + **/ +gboolean +e_client_utils_open_new_finish (ESource *source, + GAsyncResult *result, + EClient **client, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail (source != NULL, FALSE); + g_return_val_if_fail (result != NULL, FALSE); + g_return_val_if_fail (client != NULL, FALSE); + g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (source), e_client_utils_open_new), FALSE); + + *client = NULL; + simple = G_SIMPLE_ASYNC_RESULT (result); + + if (g_simple_async_result_propagate_error (simple, error)) + return FALSE; + + *client = g_object_ref (g_simple_async_result_get_op_res_gpointer (simple)); + + return *client != NULL; +} diff --git a/e-util/e-client-utils.h b/e-util/e-client-utils.h new file mode 100644 index 0000000000..3f81d1f1ff --- /dev/null +++ b/e-util/e-client-utils.h @@ -0,0 +1,64 @@ +/* + * e-client-utils.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 2011 Red Hat, Inc. (www.redhat.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CLIENT_UTILS_H +#define E_CLIENT_UTILS_H + +#include <gtk/gtk.h> +#include <libedataserver/libedataserver.h> + +G_BEGIN_DECLS + +/** + * EClientSourceType: + * + * Since: 3.2 + **/ +typedef enum { + E_CLIENT_SOURCE_TYPE_CONTACTS, + E_CLIENT_SOURCE_TYPE_EVENTS, + E_CLIENT_SOURCE_TYPE_MEMOS, + E_CLIENT_SOURCE_TYPE_TASKS, + E_CLIENT_SOURCE_TYPE_LAST +} EClientSourceType; + +EClient * e_client_utils_new (ESource *source, + EClientSourceType source_type, + GError **error); + +void e_client_utils_open_new (ESource *source, + EClientSourceType source_type, + gboolean only_if_exists, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_client_utils_open_new_finish (ESource *source, + GAsyncResult *result, + EClient **client, + GError **error); + +G_END_DECLS + +#endif /* E_CLIENT_UTILS_H */ diff --git a/e-util/e-config.h b/e-util/e-config.h index 2922a25ddb..a372601cb2 100644 --- a/e-util/e-config.h +++ b/e-util/e-config.h @@ -21,6 +21,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_CONFIG_H #define E_CONFIG_H @@ -324,7 +328,7 @@ void e_config_target_free (EConfig *config, /* To implement a basic config plugin, you just need to subclass * this and initialise the class target type tables */ -#include "e-util/e-plugin.h" +#include <e-util/e-plugin.h> typedef struct _EConfigHookGroup EConfigHookGroup; typedef struct _EConfigHook EConfigHook; diff --git a/e-util/e-contact-map-window.c b/e-util/e-contact-map-window.c new file mode 100644 index 0000000000..2e3aec5bcb --- /dev/null +++ b/e-util/e-contact-map-window.c @@ -0,0 +1,500 @@ +/* + * e-contact-map-window.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 2011 Dan Vratil <dvratil@redhat.com> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#ifdef WITH_CONTACT_MAPS + +#include "e-contact-map.h" +#include "e-contact-map-window.h" +#include "e-contact-marker.h" + +#include <champlain/champlain.h> + +#include <string.h> + +#include <glib/gi18n.h> +#include <glib-object.h> + +#define E_CONTACT_MAP_WINDOW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CONTACT_MAP_WINDOW, EContactMapWindowPrivate)) + +G_DEFINE_TYPE (EContactMapWindow, e_contact_map_window, GTK_TYPE_WINDOW) + +struct _EContactMapWindowPrivate { + EContactMap *map; + + GtkWidget *zoom_in_btn; + GtkWidget *zoom_out_btn; + + GtkWidget *search_entry; + GtkListStore *completion_model; + + GHashTable *hash_table; /* Hash table contact-name -> marker */ + + GtkWidget *spinner; + gint tasks_cnt; +}; + +enum { + SHOW_CONTACT_EDITOR, + LAST_SIGNAL +}; + +static gint signals[LAST_SIGNAL] = {0}; + +static void +marker_doubleclick_cb (ClutterActor *actor, + gpointer user_data) +{ + EContactMapWindow *window = user_data; + EContactMarker *marker; + const gchar *contact_uid; + + marker = E_CONTACT_MARKER (actor); + contact_uid = e_contact_marker_get_contact_uid (marker); + + g_signal_emit (window, signals[SHOW_CONTACT_EDITOR], 0, contact_uid); +} + +static void +book_contacts_received_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EContactMapWindow *window = user_data; + EBookClient *client = E_BOOK_CLIENT (source_object); + GSList *contacts = NULL, *p; + GError *error = NULL; + + if (!e_book_client_get_contacts_finish (client, result, &contacts, &error)) + contacts = NULL; + + if (error != NULL) { + g_warning ( + "%s: Failed to get contacts: %s", + G_STRFUNC, error->message); + g_error_free (error); + } + + for (p = contacts; p; p = p->next) + e_contact_map_add_contact ( + window->priv->map, (EContact *) p->data); + + e_client_util_free_object_slist (contacts); + g_object_unref (client); +} + +static void +contact_map_window_zoom_in_cb (GtkButton *button, + gpointer user_data) +{ + EContactMapWindow *window = user_data; + ChamplainView *view; + + view = e_contact_map_get_view (window->priv->map); + + champlain_view_zoom_in (view); +} + +static void +contact_map_window_zoom_out_cb (GtkButton *button, + gpointer user_data) +{ + EContactMapWindow *window = user_data; + ChamplainView *view; + + view = e_contact_map_get_view (window->priv->map); + + champlain_view_zoom_out (view); +} +static void +zoom_level_changed_cb (ChamplainView *view, + GParamSpec *pspec, + gpointer user_data) +{ + EContactMapWindow *window = user_data; + gint zoom_level = champlain_view_get_zoom_level (view); + + gtk_widget_set_sensitive ( + window->priv->zoom_in_btn, + (zoom_level < champlain_view_get_max_zoom_level (view))); + + gtk_widget_set_sensitive ( + window->priv->zoom_out_btn, + (zoom_level > champlain_view_get_min_zoom_level (view))); +} + +/** + * Add contact to hash_table only when EContactMap tells us + * that the contact has really been added to map. + */ +static void +map_contact_added_cb (EContactMap *map, + ClutterActor *marker, + gpointer user_data) +{ + EContactMapWindowPrivate *priv = E_CONTACT_MAP_WINDOW (user_data)->priv; + const gchar *name; + GtkTreeIter iter; + + name = champlain_label_get_text (CHAMPLAIN_LABEL (marker)); + + g_hash_table_insert ( + priv->hash_table, + g_strdup (name), marker); + + gtk_list_store_append (priv->completion_model, &iter); + gtk_list_store_set ( + priv->completion_model, &iter, + 0, name, -1); + + g_signal_connect ( + marker, "double-clicked", + G_CALLBACK (marker_doubleclick_cb), user_data); + + priv->tasks_cnt--; + if (priv->tasks_cnt == 0) { + gtk_spinner_stop (GTK_SPINNER (priv->spinner)); + gtk_widget_hide (priv->spinner); + } +} + +static void +map_contact_removed_cb (EContactMap *map, + const gchar *name, + gpointer user_data) +{ + EContactMapWindowPrivate *priv = E_CONTACT_MAP_WINDOW (user_data)->priv; + GtkTreeIter iter; + GtkTreeModel *model = GTK_TREE_MODEL (priv->completion_model); + + g_hash_table_remove (priv->hash_table, name); + + if (gtk_tree_model_get_iter_first (model, &iter)) { + do { + gchar *name_str; + gtk_tree_model_get (model, &iter, 0, &name_str, -1); + if (g_ascii_strcasecmp (name_str, name) == 0) { + g_free (name_str); + gtk_list_store_remove (priv->completion_model, &iter); + break; + } + g_free (name_str); + } while (gtk_tree_model_iter_next (model, &iter)); + } +} + +static void +map_contact_geocoding_started_cb (EContactMap *map, + ClutterActor *marker, + gpointer user_data) +{ + EContactMapWindowPrivate *priv = E_CONTACT_MAP_WINDOW (user_data)->priv; + + gtk_spinner_start (GTK_SPINNER (priv->spinner)); + gtk_widget_show (priv->spinner); + + priv->tasks_cnt++; +} + +static void +map_contact_geocoding_failed_cb (EContactMap *map, + const gchar *name, + gpointer user_data) +{ + EContactMapWindowPrivate *priv = E_CONTACT_MAP_WINDOW (user_data)->priv; + + priv->tasks_cnt--; + + if (priv->tasks_cnt == 0) { + gtk_spinner_stop (GTK_SPINNER (priv->spinner)); + gtk_widget_hide (priv->spinner); + } +} + +static void +contact_map_window_find_contact_cb (GtkButton *button, + gpointer user_data) +{ + EContactMapWindowPrivate *priv = E_CONTACT_MAP_WINDOW (user_data)->priv; + ClutterActor *marker; + + marker = g_hash_table_lookup ( + priv->hash_table, + gtk_entry_get_text (GTK_ENTRY (priv->search_entry))); + + if (marker) + e_contact_map_zoom_on_marker (priv->map, marker); +} + +static gboolean +contact_map_window_entry_key_pressed_cb (GtkWidget *entry, + GdkEventKey *event, + gpointer user_data) +{ + if (event->keyval == GDK_KEY_Return) + contact_map_window_find_contact_cb (NULL, user_data); + + return FALSE; +} + +static gboolean +entry_completion_match_selected_cb (GtkEntryCompletion *widget, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer user_data) +{ + GValue name_val = {0}; + EContactMapWindowPrivate *priv = E_CONTACT_MAP_WINDOW (user_data)->priv; + const gchar *name; + + gtk_tree_model_get_value (model, iter, 0, &name_val); + g_return_val_if_fail (G_VALUE_HOLDS_STRING (&name_val), FALSE); + + name = g_value_get_string (&name_val); + gtk_entry_set_text (GTK_ENTRY (priv->search_entry), name); + + contact_map_window_find_contact_cb (NULL, user_data); + + return TRUE; +} + +static void +contact_map_window_finalize (GObject *object) +{ + EContactMapWindowPrivate *priv; + + priv = E_CONTACT_MAP_WINDOW (object)->priv; + + if (priv->hash_table) { + g_hash_table_destroy (priv->hash_table); + priv->hash_table = NULL; + } + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_contact_map_window_parent_class)->finalize (object); +} + +static void +contact_map_window_dispose (GObject *object) +{ + EContactMapWindowPrivate *priv; + + priv = E_CONTACT_MAP_WINDOW (object)->priv; + + if (priv->map) { + gtk_widget_destroy (GTK_WIDGET (priv->map)); + priv->map = NULL; + } + + if (priv->completion_model) { + g_object_unref (priv->completion_model); + priv->completion_model = NULL; + } + + G_OBJECT_CLASS (e_contact_map_window_parent_class)->dispose (object); +} + +static void +e_contact_map_window_class_init (EContactMapWindowClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EContactMapWindowPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = contact_map_window_finalize; + object_class->dispose = contact_map_window_dispose; + + signals[SHOW_CONTACT_EDITOR] = g_signal_new ( + "show-contact-editor", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (EContactMapWindowClass, show_contact_editor), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); +} + +static void +e_contact_map_window_init (EContactMapWindow *window) +{ + EContactMapWindowPrivate *priv; + GtkWidget *map; + GtkWidget *button, *entry; + GtkWidget *hbox, *vbox, *viewport; + GtkEntryCompletion *entry_completion; + GtkListStore *completion_model; + ChamplainView *view; + GHashTable *hash_table; + + priv = E_CONTACT_MAP_WINDOW_GET_PRIVATE (window); + window->priv = priv; + + priv->tasks_cnt = 0; + + hash_table = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) NULL); + priv->hash_table = hash_table; + + gtk_window_set_title (GTK_WINDOW (window), _("Contacts Map")); + gtk_container_set_border_width (GTK_CONTAINER (window), 12); + gtk_widget_set_size_request (GTK_WIDGET (window), 800, 600); + + /* The map view itself */ + map = e_contact_map_new (); + view = e_contact_map_get_view (E_CONTACT_MAP (map)); + champlain_view_set_zoom_level (view, 2); + priv->map = E_CONTACT_MAP (map); + g_signal_connect ( + view, "notify::zoom-level", + G_CALLBACK (zoom_level_changed_cb), window); + g_signal_connect ( + map, "contact-added", + G_CALLBACK (map_contact_added_cb), window); + g_signal_connect ( + map, "contact-removed", + G_CALLBACK (map_contact_removed_cb), window); + g_signal_connect ( + map, "geocoding-started", + G_CALLBACK (map_contact_geocoding_started_cb), window); + g_signal_connect ( + map, "geocoding-failed", + G_CALLBACK (map_contact_geocoding_failed_cb), window); + + /* HBox container */ + hbox = gtk_hbox_new (FALSE, 7); + + /* Spinner */ + button = gtk_spinner_new (); + gtk_container_add (GTK_CONTAINER (hbox), button); + gtk_widget_hide (button); + priv->spinner = button; + + /* Zoom-in button */ + button = gtk_button_new_from_stock (GTK_STOCK_ZOOM_IN); + g_signal_connect ( + button, "clicked", + G_CALLBACK (contact_map_window_zoom_in_cb), window); + priv->zoom_in_btn = button; + gtk_container_add (GTK_CONTAINER (hbox), button); + + /* Zoom-out button */ + button = gtk_button_new_from_stock (GTK_STOCK_ZOOM_OUT); + g_signal_connect ( + button, "clicked", + G_CALLBACK (contact_map_window_zoom_out_cb), window); + priv->zoom_out_btn = button; + gtk_container_add (GTK_CONTAINER (hbox), button); + + /* Completion model */ + completion_model = gtk_list_store_new (1, G_TYPE_STRING); + priv->completion_model = completion_model; + + /* Entry completion */ + entry_completion = gtk_entry_completion_new (); + gtk_entry_completion_set_model ( + entry_completion, GTK_TREE_MODEL (completion_model)); + gtk_entry_completion_set_text_column (entry_completion, 0); + g_signal_connect ( + entry_completion, "match-selected", + G_CALLBACK (entry_completion_match_selected_cb), window); + + /* Search entry */ + entry = gtk_entry_new (); + gtk_entry_set_completion (GTK_ENTRY (entry), entry_completion); + g_signal_connect ( + entry, "key-press-event", + G_CALLBACK (contact_map_window_entry_key_pressed_cb), window); + window->priv->search_entry = entry; + gtk_container_add (GTK_CONTAINER (hbox), entry); + + /* Search button */ + button = gtk_button_new_from_stock (GTK_STOCK_FIND); + g_signal_connect ( + button, "clicked", + G_CALLBACK (contact_map_window_find_contact_cb), window); + gtk_container_add (GTK_CONTAINER (hbox), button); + + viewport = gtk_frame_new (NULL); + gtk_container_add (GTK_CONTAINER (viewport), map); + + vbox = gtk_vbox_new (FALSE, 6); + gtk_container_add (GTK_CONTAINER (vbox), viewport); + gtk_box_pack_end (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + gtk_container_add (GTK_CONTAINER (window), vbox); + + gtk_widget_show_all (vbox); + gtk_widget_hide (priv->spinner); +} + +EContactMapWindow * +e_contact_map_window_new (void) +{ + return g_object_new ( + E_TYPE_CONTACT_MAP_WINDOW, NULL); +} + +/** + * Gets all contacts from @book and puts them + * on the map view + */ +void +e_contact_map_window_load_addressbook (EContactMapWindow *map, + EBookClient *book_client) +{ + EBookQuery *book_query; + gchar *query_string; + + g_return_if_fail (E_IS_CONTACT_MAP_WINDOW (map)); + g_return_if_fail (E_IS_BOOK_CLIENT (book_client)); + + /* Reference book, so that it does not get deleted before the callback is + * involved. The book is unrefed in the callback */ + g_object_ref (book_client); + + book_query = e_book_query_field_exists (E_CONTACT_ADDRESS); + query_string = e_book_query_to_string (book_query); + e_book_query_unref (book_query); + + e_book_client_get_contacts ( + book_client, query_string, NULL, + book_contacts_received_cb, map); + + g_free (query_string); +} + +EContactMap * +e_contact_map_window_get_map (EContactMapWindow *window) +{ + g_return_val_if_fail (E_IS_CONTACT_MAP_WINDOW (window), NULL); + + return window->priv->map; +} + +#endif /* WITH_CONTACT_MAPS */ diff --git a/e-util/e-contact-map-window.h b/e-util/e-contact-map-window.h new file mode 100644 index 0000000000..f18def51c2 --- /dev/null +++ b/e-util/e-contact-map-window.h @@ -0,0 +1,85 @@ +/* + * e-contact-map-window.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 2011 Dan Vratil <dvratil@redhat.com> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CONTACT_MAP_WINDOW_H +#define E_CONTACT_MAP_WINDOW_H + +#ifdef WITH_CONTACT_MAPS + +#include <gtk/gtk.h> + +#include <libebook/libebook.h> + +#include <e-util/e-contact-map.h> + +/* Standard GObject macros */ +#define E_TYPE_CONTACT_MAP_WINDOW \ + (e_contact_map_window_get_type ()) +#define E_CONTACT_MAP_WINDOW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CONTACT_MAP_WINDOW, EContactMapWindow)) +#define E_CONTACT_MAP_WINDOW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CONTACT_MAP_WINDOW, EContactMapWindowClass)) +#define E_IS_CONTACT_MAP_WINDOW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CONTACT_MAP_WINDOW)) +#define E_IS_CONTACT_MAP_WINDOW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CONTACT_MAP_WINDOW)) +#define E_CONTACT_MAP_WINDOW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CONTACT_MAP_WINDOW, EContactMapWindowClass)) + +G_BEGIN_DECLS + +typedef struct _EContactMapWindow EContactMapWindow; +typedef struct _EContactMapWindowClass EContactMapWindowClass; +typedef struct _EContactMapWindowPrivate EContactMapWindowPrivate; + +struct _EContactMapWindow { + GtkWindow parent; + EContactMapWindowPrivate *priv; +}; + +struct _EContactMapWindowClass { + GtkWindowClass parent_class; + + void (*show_contact_editor) (EContactMapWindow *window, + const gchar *contact_uid); +}; + +GType e_contact_map_window_get_type (void) G_GNUC_CONST; +EContactMapWindow * e_contact_map_window_new (void); + +void e_contact_map_window_load_addressbook (EContactMapWindow *window, + EBookClient *book); + +EContactMap * e_contact_map_window_get_map (EContactMapWindow *window); + +G_END_DECLS + +#endif /* WITH_CONTACT_MAPS */ + +#endif /* E_CONTACT_MAP_WINDOW_H */ diff --git a/e-util/e-contact-map.c b/e-util/e-contact-map.c new file mode 100644 index 0000000000..24f5ac121f --- /dev/null +++ b/e-util/e-contact-map.c @@ -0,0 +1,407 @@ +/* + * e-contact-map.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 2011 Dan Vratil <dvratil@redhat.com> + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#ifdef WITH_CONTACT_MAPS + +#include "e-contact-map.h" + +#include <champlain/champlain.h> +#include <champlain-gtk/champlain-gtk.h> +#include <geoclue/geoclue-address.h> +#include <geoclue/geoclue-position.h> +#include <geocode-glib.h> + +#include <clutter/clutter.h> + +#include <string.h> +#include <glib/gi18n.h> +#include <math.h> + +#include "e-contact-marker.h" +#include "e-marshal.h" + +#define E_CONTACT_MAP_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CONTACT_MAP, EContactMapPrivate)) + +typedef struct _AsyncContext AsyncContext; + +struct _EContactMapPrivate { + GHashTable *markers; /* Hash table contact-name -> marker */ + + ChamplainMarkerLayer *marker_layer; +}; + +struct _AsyncContext { + EContactMap *map; + EContactMarker *marker; +}; + +enum { + CONTACT_ADDED, + CONTACT_REMOVED, + GEOCODING_STARTED, + GEOCODING_FAILED, + LAST_SIGNAL +}; + +static gint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_TYPE (EContactMap, e_contact_map, GTK_CHAMPLAIN_TYPE_EMBED) + +static void +async_context_free (AsyncContext *async_context) +{ + if (async_context->map != NULL) + g_object_unref (async_context->map); + + g_slice_free (AsyncContext, async_context); +} + +static void +contact_map_address_resolved_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GHashTable *resolved = NULL; + gpointer marker_ptr; + const gchar *name; + gdouble latitude, longitude; + AsyncContext *async_context = user_data; + ChamplainMarkerLayer *marker_layer; + ChamplainMarker *marker; + GError *error = NULL; + + g_return_if_fail (async_context != NULL); + g_return_if_fail (E_IS_CONTACT_MAP (async_context->map)); + g_return_if_fail (E_IS_CONTACT_MARKER (async_context->marker)); + + marker = CHAMPLAIN_MARKER (async_context->marker); + marker_layer = async_context->map->priv->marker_layer; + + /* If the marker_layer does not exist anymore, the map has + * probably been destroyed before this callback was launched. + * It's not a failure, just silently clean up what was left + * behind and pretend nothing happened. */ + + if (!CHAMPLAIN_IS_MARKER_LAYER (marker_layer)) + goto exit; + + resolved = geocode_object_resolve_finish ( + GEOCODE_OBJECT (source), result, &error); + + if (resolved == NULL || + !geocode_object_get_coords (resolved, &longitude, &latitude)) { + const gchar *name; + if (error) + g_error_free (error); + name = champlain_label_get_text (CHAMPLAIN_LABEL (marker)); + g_signal_emit ( + async_context->map, + signals[GEOCODING_FAILED], 0, name); + goto exit; + } + + /* Move the marker to resolved position */ + champlain_location_set_location ( + CHAMPLAIN_LOCATION (marker), latitude, longitude); + champlain_marker_layer_add_marker (marker_layer, marker); + champlain_marker_set_selected (marker, FALSE); + + /* Store the marker in the hash table. Use it's label as key */ + name = champlain_label_get_text (CHAMPLAIN_LABEL (marker)); + marker_ptr = g_hash_table_lookup ( + async_context->map->priv->markers, name); + if (marker_ptr != NULL) { + g_hash_table_remove (async_context->map->priv->markers, name); + champlain_marker_layer_remove_marker (marker_layer, marker_ptr); + } + g_hash_table_insert ( + async_context->map->priv->markers, + g_strdup (name), marker); + + g_signal_emit ( + async_context->map, + signals[CONTACT_ADDED], 0, marker); + +exit: + async_context_free (async_context); + + if (resolved != NULL) + g_hash_table_unref (resolved); +} + +static void +resolve_marker_position (EContactMap *map, + EContactMarker *marker, + EContactAddress *address) +{ + GeocodeObject *geocoder; + AsyncContext *async_context; + const gchar *key; + + g_return_if_fail (E_IS_CONTACT_MAP (map)); + g_return_if_fail (address != NULL); + + geocoder = geocode_object_new (); + + key = GEOCODE_OBJECT_FIELD_POSTAL; + geocode_object_add (geocoder, key, address->code); + + key = GEOCODE_OBJECT_FIELD_COUNTRY; + geocode_object_add (geocoder, key, address->country); + + key = GEOCODE_OBJECT_FIELD_STATE; + geocode_object_add (geocoder, key, address->region); + + key = GEOCODE_OBJECT_FIELD_CITY; + geocode_object_add (geocoder, key, address->locality); + + key = GEOCODE_OBJECT_FIELD_STREET; + geocode_object_add (geocoder, key, address->street); + + async_context = g_slice_new0 (AsyncContext); + async_context->map = g_object_ref (map); + async_context->marker = marker; + + geocode_object_resolve_async ( + geocoder, NULL, + contact_map_address_resolved_cb, + async_context); + + g_object_unref (geocoder); + + g_signal_emit (map, signals[GEOCODING_STARTED], 0, marker); +} + +static void +contact_map_finalize (GObject *object) +{ + EContactMapPrivate *priv; + + priv = E_CONTACT_MAP (object)->priv; + + if (priv->markers) { + g_hash_table_destroy (priv->markers); + priv->markers = NULL; + } + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_contact_map_parent_class)->finalize (object); +} + +static void +e_contact_map_class_init (EContactMapClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EContactMapPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = contact_map_finalize; + + signals[CONTACT_ADDED] = g_signal_new ( + "contact-added", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (EContactMapClass, contact_added), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, G_TYPE_OBJECT); + + signals[CONTACT_REMOVED] = g_signal_new ( + "contact-removed", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (EContactMapClass, contact_removed), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); + + signals[GEOCODING_STARTED] = g_signal_new ( + "geocoding-started", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (EContactMapClass, geocoding_started), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, G_TYPE_OBJECT); + + signals[GEOCODING_FAILED] = g_signal_new ( + "geocoding-failed", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (EContactMapClass, geocoding_failed), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); +} + +static void +e_contact_map_init (EContactMap *map) +{ + GHashTable *hash_table; + ChamplainMarkerLayer *layer; + ChamplainView *view; + + map->priv = E_CONTACT_MAP_GET_PRIVATE (map); + + hash_table = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, NULL); + + map->priv->markers = hash_table; + + view = gtk_champlain_embed_get_view (GTK_CHAMPLAIN_EMBED (map)); + /* This feature is somehow broken sometimes, so disable it for now */ + champlain_view_set_zoom_on_double_click (view, FALSE); + layer = champlain_marker_layer_new_full (CHAMPLAIN_SELECTION_SINGLE); + champlain_view_add_layer (view, CHAMPLAIN_LAYER (layer)); + map->priv->marker_layer = layer; +} + +GtkWidget * +e_contact_map_new (void) +{ + return g_object_new ( + E_TYPE_CONTACT_MAP,NULL); +} + +void +e_contact_map_add_contact (EContactMap *map, + EContact *contact) +{ + EContactAddress *address; + EContactPhoto *photo; + const gchar *contact_uid; + gchar *name; + + g_return_if_fail (map && E_IS_CONTACT_MAP (map)); + g_return_if_fail (contact && E_IS_CONTACT (contact)); + + photo = e_contact_get (contact, E_CONTACT_PHOTO); + contact_uid = e_contact_get_const (contact, E_CONTACT_UID); + + address = e_contact_get (contact, E_CONTACT_ADDRESS_HOME); + if (address) { + name = g_strconcat (e_contact_get_const (contact, E_CONTACT_FILE_AS), " (", _("Home"), ")", NULL); + e_contact_map_add_marker (map, name, contact_uid, address, photo); + g_free (name); + e_contact_address_free (address); + } + + address = e_contact_get (contact, E_CONTACT_ADDRESS_WORK); + if (address) { + name = g_strconcat (e_contact_get_const (contact, E_CONTACT_FILE_AS), " (", _("Work"), ")", NULL); + e_contact_map_add_marker (map, name, contact_uid, address, photo); + g_free (name); + e_contact_address_free (address); + } + + if (photo) + e_contact_photo_free (photo); +} + +void +e_contact_map_add_marker (EContactMap *map, + const gchar *name, + const gchar *contact_uid, + EContactAddress *address, + EContactPhoto *photo) +{ + EContactMarker *marker; + + g_return_if_fail (map && E_IS_CONTACT_MAP (map)); + g_return_if_fail (name && *name); + g_return_if_fail (contact_uid && *contact_uid); + g_return_if_fail (address); + + marker = E_CONTACT_MARKER (e_contact_marker_new (name, contact_uid, photo)); + + resolve_marker_position (map, marker, address); +} + +/** + * The \name parameter must match the label of the + * marker (for example "John Smith (work)") + */ +void +e_contact_map_remove_contact (EContactMap *map, + const gchar *name) +{ + ChamplainMarker *marker; + + g_return_if_fail (map && E_IS_CONTACT_MAP (map)); + g_return_if_fail (name && *name); + + marker = g_hash_table_lookup (map->priv->markers, name); + + champlain_marker_layer_remove_marker (map->priv->marker_layer, marker); + + g_hash_table_remove (map->priv->markers, name); + + g_signal_emit (map, signals[CONTACT_REMOVED], 0, name); +} + +void +e_contact_map_remove_marker (EContactMap *map, + ClutterActor *marker) +{ + const gchar *name; + + g_return_if_fail (map && E_IS_CONTACT_MAP (map)); + g_return_if_fail (marker && CLUTTER_IS_ACTOR (marker)); + + name = champlain_label_get_text (CHAMPLAIN_LABEL (marker)); + + e_contact_map_remove_contact (map, name); +} + +void +e_contact_map_zoom_on_marker (EContactMap *map, + ClutterActor *marker) +{ + ChamplainView *view; + gdouble lat, lng; + + g_return_if_fail (map && E_IS_CONTACT_MAP (map)); + g_return_if_fail (marker && CLUTTER_IS_ACTOR (marker)); + + lat = champlain_location_get_latitude (CHAMPLAIN_LOCATION (marker)); + lng = champlain_location_get_longitude (CHAMPLAIN_LOCATION (marker)); + + view = gtk_champlain_embed_get_view (GTK_CHAMPLAIN_EMBED (map)); + + champlain_view_center_on (view, lat, lng); + champlain_view_set_zoom_level (view, 15); +} + +ChamplainView * +e_contact_map_get_view (EContactMap *map) +{ + g_return_val_if_fail (E_IS_CONTACT_MAP (map), NULL); + + return gtk_champlain_embed_get_view (GTK_CHAMPLAIN_EMBED (map)); +} + +#endif /* WITH_CONTACT_MAPS */ diff --git a/e-util/e-contact-map.h b/e-util/e-contact-map.h new file mode 100644 index 0000000000..90b7a6a911 --- /dev/null +++ b/e-util/e-contact-map.h @@ -0,0 +1,110 @@ +/* + * e-contact-map.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 2011 Dan Vratil <dvratil@redhat.com> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CONTACT_MAP_H +#define E_CONTACT_MAP_H + +#ifdef WITH_CONTACT_MAPS + +#include <gtk/gtk.h> + +#include <champlain/champlain.h> +#include <champlain-gtk/champlain-gtk.h> + +#include <libebook/libebook.h> + +/* Standard GObject macros */ +#define E_TYPE_CONTACT_MAP \ + (e_contact_map_get_type ()) +#define E_CONTACT_MAP(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CONTACT_MAP, EContactMap)) +#define E_CONTACT_MAP_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CONTACT_MAP, EContactMapClass)) +#define E_IS_CONTACT_MAP(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CONTACT_MAP)) +#define E_IS_CONTACT_MAP_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CONTACT_MAP)) +#define E_CONTACT_MAP_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CONTACT_MAP, EContactMapClass)) + +G_BEGIN_DECLS + +typedef struct _EContactMap EContactMap; +typedef struct _EContactMapClass EContactMapClass; +typedef struct _EContactMapPrivate EContactMapPrivate; + +struct _EContactMap { + GtkChamplainEmbed parent; + EContactMapPrivate *priv; +}; + +struct _EContactMapClass { + GtkWindowClass parent_class; + + void (*contact_added) (EContactMap *map, + ClutterActor *marker); + + void (*contact_removed) (EContactMap *map, + const gchar *name); + + void (*geocoding_started) (EContactMap *map, + ClutterActor *marker); + + void (*geocoding_failed) (EContactMap *map, + const gchar *name); +}; + +GType e_contact_map_get_type (void) G_GNUC_CONST; +GtkWidget * e_contact_map_new (void); + +void e_contact_map_add_contact (EContactMap *map, + EContact *contact); + +void e_contact_map_add_marker (EContactMap *map, + const gchar *name, + const gchar *contact_uid, + EContactAddress *address, + EContactPhoto *photo); + +void e_contact_map_remove_contact (EContactMap *map, + const gchar *name); + +void e_contact_map_remove_marker (EContactMap *map, + ClutterActor *marker); + +void e_contact_map_zoom_on_marker (EContactMap *map, + ClutterActor *marker); + +ChamplainView * e_contact_map_get_view (EContactMap *map); + +G_END_DECLS + +#endif /* WITH_CONTACT_MAPS */ + +#endif diff --git a/e-util/e-contact-marker.c b/e-util/e-contact-marker.c new file mode 100644 index 0000000000..9ac9417c9f --- /dev/null +++ b/e-util/e-contact-marker.c @@ -0,0 +1,624 @@ +/* + * e-contact-marker.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 2008 Pierre-Luc Beaudoin <pierre-luc@pierlux.com> + * Copyright (C) 2011 Jiri Techet <techet@gmail.com> + * Copyright (C) 2011 Dan Vratil <dvratil@redhat.com> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#ifdef WITH_CONTACT_MAPS + +#include "e-contact-marker.h" + +#include <champlain/champlain.h> +#include <gtk/gtk.h> +#include <clutter/clutter.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <glib-object.h> +#include <cairo.h> +#include <math.h> +#include <string.h> + +#define E_CONTACT_MARKER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CONTACT_MARKER, EContactMarkerPrivate)) + +G_DEFINE_TYPE (EContactMarker, e_contact_marker, CHAMPLAIN_TYPE_LABEL); + +struct _EContactMarkerPrivate +{ + gchar *contact_uid; + + ClutterActor *image; + ClutterActor *text_actor; + + ClutterActor *shadow; + ClutterActor *background; + + guint total_width; + guint total_height; + + ClutterGroup *content_group; + + guint redraw_id; +}; + +enum { + DOUBLE_CLICKED, + LAST_SIGNAL +}; + +static gint signals[LAST_SIGNAL] = {0}; + +#define DEFAULT_FONT_NAME "Serif 9" + +static ClutterColor DEFAULT_COLOR = { 0x33, 0x33, 0x33, 0xff }; + +#define RADIUS 10 +#define PADDING (RADIUS / 2) + +static gboolean +contact_marker_clicked_cb (ClutterActor *actor, + ClutterEvent *event, + gpointer user_data) +{ + gint click_count = clutter_event_get_click_count (event); + + if (click_count == 2) + g_signal_emit (E_CONTACT_MARKER (actor), signals[DOUBLE_CLICKED], 0); + + return TRUE; +} + +static ClutterActor * +texture_new_from_pixbuf (GdkPixbuf *pixbuf, + GError **error) +{ + ClutterActor *texture = NULL; + const guchar *data; + gboolean has_alpha, success; + gint width, height, rowstride; + ClutterTextureFlags flags = 0; + + data = gdk_pixbuf_get_pixels (pixbuf); + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + has_alpha = gdk_pixbuf_get_has_alpha (pixbuf); + rowstride = gdk_pixbuf_get_rowstride (pixbuf); + + texture = clutter_texture_new (); + success = clutter_texture_set_from_rgb_data ( + CLUTTER_TEXTURE (texture), + data, has_alpha, width, height, rowstride, + (has_alpha ? 4: 3), flags, NULL); + + if (!success) { + clutter_actor_destroy (CLUTTER_ACTOR (texture)); + texture = NULL; + } + + return texture; +} + +static ClutterActor * +contact_photo_to_texture (EContactPhoto *photo) +{ + GdkPixbuf *pixbuf; + + if (photo->type == E_CONTACT_PHOTO_TYPE_INLINED) { + GError *error = NULL; + + GdkPixbufLoader *loader = gdk_pixbuf_loader_new (); + gdk_pixbuf_loader_write ( + loader, photo->data.inlined.data, + photo->data.inlined.length, NULL); + gdk_pixbuf_loader_close (loader, NULL); + pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); + if (pixbuf) + g_object_ref (pixbuf); + g_object_unref (loader); + + if (error) { + g_error_free (error); + return NULL; + } + } else if (photo->type == E_CONTACT_PHOTO_TYPE_URI) { + GError *error = NULL; + + pixbuf = gdk_pixbuf_new_from_file (photo->data.uri, &error); + + if (error) { + g_error_free (error); + return NULL; + } + } else + return NULL; + + if (pixbuf) { + ClutterActor *texture; + GError *error = NULL; + + texture = texture_new_from_pixbuf (pixbuf, &error); + if (error) { + g_error_free (error); + g_object_unref (pixbuf); + return NULL; + } + + g_object_unref (pixbuf); + return texture; + } + + return NULL; +} + +static void +draw_box (cairo_t *cr, + gint width, + gint height, + gint point) +{ + cairo_move_to (cr, RADIUS, 0); + cairo_line_to (cr, width - RADIUS, 0); + cairo_arc (cr, width - RADIUS, RADIUS, RADIUS - 1, 3 * M_PI / 2.0, 0); + cairo_line_to (cr, width, height - RADIUS); + cairo_arc (cr, width - RADIUS, height - RADIUS, RADIUS - 1, 0, M_PI / 2.0); + cairo_line_to (cr, point, height); + cairo_line_to (cr, 0, height + point); + cairo_arc (cr, RADIUS, RADIUS, RADIUS - 1, M_PI, 3 * M_PI / 2.0); + cairo_close_path (cr); +} + +static void +draw_shadow (EContactMarker *marker, + gint width, + gint height, + gint point) +{ + EContactMarkerPrivate *priv = marker->priv; + ClutterActor *shadow = NULL; + cairo_t *cr; + gdouble slope; + gdouble scaling; + gint x; + cairo_matrix_t matrix; + + slope = -0.3; + scaling = 0.65; + x = -40 * slope; + + shadow = clutter_cairo_texture_new (width + x, (height + point)); + cr = clutter_cairo_texture_create (CLUTTER_CAIRO_TEXTURE (shadow)); + + cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR); + cairo_paint (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_OVER); + + cairo_matrix_init (&matrix, 1, 0, slope, scaling, x, 0); + cairo_set_matrix (cr, &matrix); + + draw_box (cr, width, height, point); + + cairo_set_source_rgba (cr, 0, 0, 0, 0.15); + cairo_fill (cr); + + cairo_destroy (cr); + + clutter_actor_set_position (shadow, 0, height / 2.0); + + clutter_container_add_actor (CLUTTER_CONTAINER (priv->content_group), shadow); + + if (priv->shadow != NULL) { + clutter_container_remove_actor ( + CLUTTER_CONTAINER (priv->content_group), + priv->shadow); + } + + priv->shadow = shadow; +} + +static void +draw_background (EContactMarker *marker, + gint width, + gint height, + gint point) +{ + EContactMarkerPrivate *priv = marker->priv; + ClutterActor *bg = NULL; + const ClutterColor *color; + ClutterColor darker_color; + cairo_t *cr; + + bg = clutter_cairo_texture_new (width, height + point); + cr = clutter_cairo_texture_create (CLUTTER_CAIRO_TEXTURE (bg)); + + cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR); + cairo_paint (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_OVER); + + /* If selected, add the selection color to the marker's color */ + if (champlain_marker_get_selected (CHAMPLAIN_MARKER (marker))) + color = champlain_marker_get_selection_color (); + else + color = &DEFAULT_COLOR; + + draw_box (cr, width, height, point); + + clutter_color_darken (color, &darker_color); + + cairo_set_source_rgba ( + cr, + color->red / 255.0, + color->green / 255.0, + color->blue / 255.0, + color->alpha / 255.0); + cairo_fill_preserve (cr); + + cairo_set_line_width (cr, 1.0); + cairo_set_source_rgba ( + cr, + darker_color.red / 255.0, + darker_color.green / 255.0, + darker_color.blue / 255.0, + darker_color.alpha / 255.0); + cairo_stroke (cr); + cairo_destroy (cr); + + clutter_container_add_actor (CLUTTER_CONTAINER (priv->content_group), bg); + + if (priv->background != NULL) { + clutter_container_remove_actor ( + CLUTTER_CONTAINER (priv->content_group), + priv->background); + } + + priv->background = bg; +} + +static void +draw_marker (EContactMarker *marker) +{ + EContactMarkerPrivate *priv = marker->priv; + ChamplainLabel *label = CHAMPLAIN_LABEL (marker); + guint height = 0, point = 0; + guint total_width = 0, total_height = 0; + ClutterText *text; + + if (priv->image) { + clutter_actor_set_position (priv->image, 2 *PADDING, 2 *PADDING); + if (clutter_actor_get_parent (priv->image) == NULL) + clutter_container_add_actor ( + CLUTTER_CONTAINER (priv->content_group), + priv->image); + } + + if (priv->text_actor == NULL) { + priv->text_actor = clutter_text_new_with_text ( + "Serif 8", + champlain_label_get_text (label)); + champlain_label_set_font_name (label, "Serif 8"); + } + + text = CLUTTER_TEXT (priv->text_actor); + clutter_text_set_text ( + text, + champlain_label_get_text (label)); + clutter_text_set_font_name ( + text, + champlain_label_get_font_name (label)); + clutter_text_set_line_alignment (text, PANGO_ALIGN_CENTER); + clutter_text_set_line_wrap (text, TRUE); + clutter_text_set_line_wrap_mode (text, PANGO_WRAP_WORD); + clutter_text_set_ellipsize ( + text, + champlain_label_get_ellipsize (label)); + clutter_text_set_attributes ( + text, + champlain_label_get_attributes (label)); + clutter_text_set_use_markup ( + text, + champlain_label_get_use_markup (label)); + + if (priv->image) { + clutter_actor_set_width ( + priv->text_actor, + clutter_actor_get_width (priv->image)); + total_height = clutter_actor_get_height (priv->image) + 2 *PADDING + + clutter_actor_get_height (priv->text_actor) + 2 *PADDING; + total_width = clutter_actor_get_width (priv->image) + 4 *PADDING; + clutter_actor_set_position ( + priv->text_actor, PADDING, + clutter_actor_get_height (priv->image) + 2 *PADDING + 3); + } else { + total_height = clutter_actor_get_height (priv->text_actor) + 2 *PADDING; + total_width = clutter_actor_get_width (priv->text_actor) + 4 *PADDING; + clutter_actor_set_position (priv->text_actor, 2 * PADDING, PADDING); + } + + height += 2 * PADDING; + if (height > total_height) + total_height = height; + + clutter_text_set_color ( + CLUTTER_TEXT (priv->text_actor), + (champlain_marker_get_selected (CHAMPLAIN_MARKER (marker)) ? + champlain_marker_get_selection_text_color () : + champlain_label_get_text_color (CHAMPLAIN_LABEL (marker)))); + if (clutter_actor_get_parent (priv->text_actor) == NULL) + clutter_container_add_actor ( + CLUTTER_CONTAINER (priv->content_group), + priv->text_actor); + + if (priv->text_actor == NULL && priv->image == NULL) { + total_width = 6 * PADDING; + total_height = 6 * PADDING; + } + + point = (total_height + 2 * PADDING) / 4.0; + priv->total_width = total_width; + priv->total_height = total_height; + + draw_shadow (marker, total_width, total_height, point); + draw_background (marker, total_width, total_height, point); + + if (priv->text_actor != NULL && priv->background != NULL) + clutter_actor_raise (priv->text_actor, priv->background); + if (priv->image != NULL && priv->background != NULL) + clutter_actor_raise (priv->image, priv->background); + + clutter_actor_set_anchor_point (CLUTTER_ACTOR (marker), 0, total_height + point); +} + +static gboolean +redraw_on_idle (gpointer gobject) +{ + EContactMarker *marker = E_CONTACT_MARKER (gobject); + + draw_marker (marker); + marker->priv->redraw_id = 0; + return FALSE; +} + +static void +queue_redraw (EContactMarker *marker) +{ + EContactMarkerPrivate *priv = marker->priv; + + if (!priv->redraw_id) { + priv->redraw_id = g_idle_add_full ( + G_PRIORITY_DEFAULT, + (GSourceFunc) redraw_on_idle, + g_object_ref (marker), + (GDestroyNotify) g_object_unref); + } +} + +static void +allocate (ClutterActor *self, + const ClutterActorBox *box, + ClutterAllocationFlags flags) +{ + ClutterActorBox child_box; + EContactMarkerPrivate *priv = E_CONTACT_MARKER (self)->priv; + + CLUTTER_ACTOR_CLASS (e_contact_marker_parent_class)->allocate (self, box, flags); + + child_box.x1 = 0; + child_box.x2 = box->x2 - box->x1; + child_box.y1 = 0; + child_box.y2 = box->y2 - box->y1; + clutter_actor_allocate (CLUTTER_ACTOR (priv->content_group), &child_box, flags); +} + +static void +paint (ClutterActor *self) +{ + EContactMarkerPrivate *priv = E_CONTACT_MARKER (self)->priv; + + clutter_actor_paint (CLUTTER_ACTOR (priv->content_group)); +} + +static void +map (ClutterActor *self) +{ + EContactMarkerPrivate *priv = E_CONTACT_MARKER (self)->priv; + + CLUTTER_ACTOR_CLASS (e_contact_marker_parent_class)->map (self); + + clutter_actor_map (CLUTTER_ACTOR (priv->content_group)); +} + +static void +unmap (ClutterActor *self) +{ + EContactMarkerPrivate *priv = E_CONTACT_MARKER (self)->priv; + + CLUTTER_ACTOR_CLASS (e_contact_marker_parent_class)->unmap (self); + + clutter_actor_unmap (CLUTTER_ACTOR (priv->content_group)); +} + +static void +pick (ClutterActor *self, + const ClutterColor *color) +{ + EContactMarkerPrivate *priv = E_CONTACT_MARKER (self)->priv; + gfloat width, height; + + if (!clutter_actor_should_pick_paint (self)) + return; + + width = priv->total_width; + height = priv->total_height; + + cogl_path_new (); + + cogl_set_source_color4ub ( + color->red, + color->green, + color->blue, + color->alpha); + + cogl_path_move_to (RADIUS, 0); + cogl_path_line_to (width - RADIUS, 0); + cogl_path_arc (width - RADIUS, RADIUS, RADIUS, RADIUS, -90, 0); + cogl_path_line_to (width, height - RADIUS); + cogl_path_arc (width - RADIUS, height - RADIUS, RADIUS, RADIUS, 0, 90); + cogl_path_line_to (RADIUS, height); + cogl_path_arc (RADIUS, height - RADIUS, RADIUS, RADIUS, 90, 180); + cogl_path_line_to (0, RADIUS); + cogl_path_arc (RADIUS, RADIUS, RADIUS, RADIUS, 180, 270); + cogl_path_close (); + cogl_path_fill (); +} + +static void +notify_selected (GObject *gobject, + G_GNUC_UNUSED GParamSpec *pspec, + G_GNUC_UNUSED gpointer user_data) +{ + queue_redraw (E_CONTACT_MARKER (gobject)); +} + +static void +e_contact_marker_finalize (GObject *object) +{ + EContactMarkerPrivate *priv = E_CONTACT_MARKER (object)->priv; + + if (priv->contact_uid) { + g_free (priv->contact_uid); + priv->contact_uid = NULL; + } + + if (priv->redraw_id) { + g_source_remove (priv->redraw_id); + priv->redraw_id = 0; + } + + G_OBJECT_CLASS (e_contact_marker_parent_class)->finalize (object); +} + +static void +e_contact_marker_dispose (GObject *object) +{ + EContactMarkerPrivate *priv = E_CONTACT_MARKER (object)->priv; + + priv->background = NULL; + priv->shadow = NULL; + priv->text_actor = NULL; + + if (priv->content_group) { + clutter_actor_unparent (CLUTTER_ACTOR (priv->content_group)); + priv->content_group = NULL; + } + + G_OBJECT_CLASS (e_contact_marker_parent_class)->dispose (object); +} + +static void +e_contact_marker_class_init (EContactMarkerClass *class) +{ + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + g_type_class_add_private (class, sizeof (EContactMarkerPrivate)); + + object_class->dispose = e_contact_marker_dispose; + object_class->finalize = e_contact_marker_finalize; + + actor_class->paint = paint; + actor_class->allocate = allocate; + actor_class->map = map; + actor_class->unmap = unmap; + actor_class->pick = pick; + + signals[DOUBLE_CLICKED] = g_signal_new ( + "double-clicked", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (EContactMarkerClass, double_clicked), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_contact_marker_init (EContactMarker *marker) +{ + EContactMarkerPrivate *priv; + + priv = E_CONTACT_MARKER_GET_PRIVATE (marker); + + marker->priv = priv; + priv->contact_uid = NULL; + priv->image = NULL; + priv->background = NULL; + priv->shadow = NULL; + priv->text_actor = NULL; + priv->content_group = CLUTTER_GROUP (clutter_group_new ()); + priv->redraw_id = 0; + + clutter_actor_set_parent ( + CLUTTER_ACTOR (priv->content_group), CLUTTER_ACTOR (marker)); + clutter_actor_queue_relayout (CLUTTER_ACTOR (marker)); + + priv->total_width = 0; + priv->total_height = 0; + + g_signal_connect ( + marker, "notify::selected", + G_CALLBACK (notify_selected), NULL); + g_signal_connect ( + marker, "button-release-event", + G_CALLBACK (contact_marker_clicked_cb), NULL); +} + +ClutterActor * +e_contact_marker_new (const gchar *name, + const gchar *contact_uid, + EContactPhoto *photo) +{ + ClutterActor *marker = CLUTTER_ACTOR (g_object_new (E_TYPE_CONTACT_MARKER, NULL)); + EContactMarkerPrivate *priv = E_CONTACT_MARKER (marker)->priv; + + g_return_val_if_fail (name && *name, NULL); + g_return_val_if_fail (contact_uid && *contact_uid, NULL); + + champlain_label_set_text (CHAMPLAIN_LABEL (marker), name); + priv->contact_uid = g_strdup (contact_uid); + if (photo) + priv->image = contact_photo_to_texture (photo); + + queue_redraw (E_CONTACT_MARKER (marker)); + + return marker; +} + +const gchar * +e_contact_marker_get_contact_uid (EContactMarker *marker) +{ + g_return_val_if_fail (marker && E_IS_CONTACT_MARKER (marker), NULL); + + return marker->priv->contact_uid; +} + +#endif /* WITH_CONTACT_MAPS */ diff --git a/e-util/e-contact-marker.h b/e-util/e-contact-marker.h new file mode 100644 index 0000000000..e6e10db855 --- /dev/null +++ b/e-util/e-contact-marker.h @@ -0,0 +1,88 @@ +/* + * e-contact-marker.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 2008 Pierre-Luc Beaudoin <pierre-luc@pierlux.com> + * Copyright (C) 2011 Jiri Techet <techet@gmail.com> + * Copyright (C) 2011 Dan Vratil <dvratil@redhat.com> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CONTACT_MARKER_H +#define E_CONTACT_MARKER_H + +#ifdef WITH_CONTACT_MAPS + +#include <libebook/libebook.h> + +#include <champlain/champlain.h> + +#include <glib-object.h> +#include <clutter/clutter.h> + +G_BEGIN_DECLS + +#define E_TYPE_CONTACT_MARKER e_contact_marker_get_type () + +#define E_CONTACT_MARKER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_TYPE_CONTACT_MARKER, EContactMarker)) + +#define E_CONTACT_MARKER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), E_TYPE_CONTACT_MARKER, EContactMarkerClass)) + +#define E_IS_CONTACT_MARKER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_CONTACT_MARKER)) + +#define E_IS_CONTACT_MARKER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), E_TYPE_CONTACT_MARKER)) + +#define E_CONTACT_MARKER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), E_TYPE_CONTACT_MARKER, EContactMarkerClass)) + +typedef struct _EContactMarkerPrivate EContactMarkerPrivate; + +typedef struct _EContactMarker EContactMarker; +typedef struct _EContactMarkerClass EContactMarkerClass; + +struct _EContactMarker +{ + ChamplainLabel parent; + EContactMarkerPrivate *priv; +}; + +struct _EContactMarkerClass +{ + ChamplainLabelClass parent_class; + + void (*double_clicked) (ClutterActor *actor); +}; + +GType e_contact_marker_get_type (void); + +ClutterActor * e_contact_marker_new (const gchar *name, + const gchar *contact_uid, + EContactPhoto *photo); + +const gchar * e_contact_marker_get_contact_uid (EContactMarker *marker); + +G_END_DECLS + +#endif /* WITH_CONTACT_MAPS */ + +#endif diff --git a/e-util/e-contact-store.c b/e-util/e-contact-store.c new file mode 100644 index 0000000000..4e49399e82 --- /dev/null +++ b/e-util/e-contact-store.c @@ -0,0 +1,1370 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-contact-store.c - Contacts store with GtkTreeModel interface. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <string.h> +#include <glib/gi18n-lib.h> + +#include "e-contact-store.h" + +#define ITER_IS_VALID(contact_store, iter) \ + ((iter)->stamp == (contact_store)->priv->stamp) +#define ITER_GET(iter) \ + GPOINTER_TO_INT (iter->user_data) +#define ITER_SET(contact_store, iter, index) \ + G_STMT_START { \ + (iter)->stamp = (contact_store)->priv->stamp; \ + (iter)->user_data = GINT_TO_POINTER (index); \ + } G_STMT_END + +#define E_CONTACT_STORE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CONTACT_STORE, EContactStorePrivate)) + +struct _EContactStorePrivate { + gint stamp; + EBookQuery *query; + GArray *contact_sources; +}; + +/* Signals */ + +enum { + START_CLIENT_VIEW, + STOP_CLIENT_VIEW, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static void e_contact_store_tree_model_init (GtkTreeModelIface *iface); + +G_DEFINE_TYPE_WITH_CODE ( + EContactStore, e_contact_store, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, e_contact_store_tree_model_init)) + +static GtkTreeModelFlags e_contact_store_get_flags (GtkTreeModel *tree_model); +static gint e_contact_store_get_n_columns (GtkTreeModel *tree_model); +static GType e_contact_store_get_column_type (GtkTreeModel *tree_model, + gint index); +static gboolean e_contact_store_get_iter (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreePath *path); +static GtkTreePath *e_contact_store_get_path (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static void e_contact_store_get_value (GtkTreeModel *tree_model, + GtkTreeIter *iter, + gint column, + GValue *value); +static gboolean e_contact_store_iter_next (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static gboolean e_contact_store_iter_children (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent); +static gboolean e_contact_store_iter_has_child (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static gint e_contact_store_iter_n_children (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static gboolean e_contact_store_iter_nth_child (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent, + gint n); +static gboolean e_contact_store_iter_parent (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *child); + +typedef struct +{ + EBookClient *book_client; + + EBookClientView *client_view; + GPtrArray *contacts; + + EBookClientView *client_view_pending; + GPtrArray *contacts_pending; +} +ContactSource; + +static void free_contact_ptrarray (GPtrArray *contacts); +static void clear_contact_source (EContactStore *contact_store, ContactSource *source); +static void stop_view (EContactStore *contact_store, EBookClientView *view); + +static void +contact_store_dispose (GObject *object) +{ + EContactStorePrivate *priv; + gint ii; + + priv = E_CONTACT_STORE_GET_PRIVATE (object); + + /* Free sources and cached contacts */ + for (ii = 0; ii < priv->contact_sources->len; ii++) { + ContactSource *source; + + /* clear from back, because clear_contact_source can later access freed memory */ + source = &g_array_index ( + priv->contact_sources, ContactSource, priv->contact_sources->len - ii - 1); + + clear_contact_source (E_CONTACT_STORE (object), source); + free_contact_ptrarray (source->contacts); + g_object_unref (source->book_client); + } + g_array_set_size (priv->contact_sources, 0); + + if (priv->query != NULL) { + e_book_query_unref (priv->query); + priv->query = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_contact_store_parent_class)->dispose (object); +} + +static void +contact_store_finalize (GObject *object) +{ + EContactStorePrivate *priv; + + priv = E_CONTACT_STORE_GET_PRIVATE (object); + + g_array_free (priv->contact_sources, TRUE); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_contact_store_parent_class)->finalize (object); +} + +static void +e_contact_store_class_init (EContactStoreClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EContactStorePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = contact_store_dispose; + object_class->finalize = contact_store_finalize; + + signals[START_CLIENT_VIEW] = g_signal_new ( + "start-client-view", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EContactStoreClass, start_client_view), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + E_TYPE_BOOK_CLIENT_VIEW); + + signals[STOP_CLIENT_VIEW] = g_signal_new ( + "stop-client-view", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EContactStoreClass, stop_client_view), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + E_TYPE_BOOK_CLIENT_VIEW); +} + +static void +e_contact_store_tree_model_init (GtkTreeModelIface *iface) +{ + iface->get_flags = e_contact_store_get_flags; + iface->get_n_columns = e_contact_store_get_n_columns; + iface->get_column_type = e_contact_store_get_column_type; + iface->get_iter = e_contact_store_get_iter; + iface->get_path = e_contact_store_get_path; + iface->get_value = e_contact_store_get_value; + iface->iter_next = e_contact_store_iter_next; + iface->iter_children = e_contact_store_iter_children; + iface->iter_has_child = e_contact_store_iter_has_child; + iface->iter_n_children = e_contact_store_iter_n_children; + iface->iter_nth_child = e_contact_store_iter_nth_child; + iface->iter_parent = e_contact_store_iter_parent; +} + +static void +e_contact_store_init (EContactStore *contact_store) +{ + GArray *contact_sources; + + contact_sources = g_array_new (FALSE, FALSE, sizeof (ContactSource)); + + contact_store->priv = E_CONTACT_STORE_GET_PRIVATE (contact_store); + contact_store->priv->stamp = g_random_int (); + contact_store->priv->contact_sources = contact_sources; +} + +/** + * e_contact_store_new: + * + * Creates a new #EContactStore. + * + * Returns: A new #EContactStore. + **/ +EContactStore * +e_contact_store_new (void) +{ + return g_object_new (E_TYPE_CONTACT_STORE, NULL); +} + +/* ------------------ * + * Row update helpers * + * ------------------ */ + +static void +row_deleted (EContactStore *contact_store, + gint n) +{ + GtkTreePath *path; + + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, n); + gtk_tree_model_row_deleted (GTK_TREE_MODEL (contact_store), path); + gtk_tree_path_free (path); +} + +static void +row_inserted (EContactStore *contact_store, + gint n) +{ + GtkTreePath *path; + GtkTreeIter iter; + + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, n); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (contact_store), &iter, path)) + gtk_tree_model_row_inserted (GTK_TREE_MODEL (contact_store), path, &iter); + + gtk_tree_path_free (path); +} + +static void +row_changed (EContactStore *contact_store, + gint n) +{ + GtkTreePath *path; + GtkTreeIter iter; + + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, n); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (contact_store), &iter, path)) + gtk_tree_model_row_changed (GTK_TREE_MODEL (contact_store), path, &iter); + + gtk_tree_path_free (path); +} + +/* ---------------------- * + * Contact source helpers * + * ---------------------- */ + +static gint +find_contact_source_by_client (EContactStore *contact_store, + EBookClient *book_client) +{ + GArray *array; + gint i; + + array = contact_store->priv->contact_sources; + + for (i = 0; i < array->len; i++) { + ContactSource *source; + + source = &g_array_index (array, ContactSource, i); + if (source->book_client == book_client) + return i; + } + + return -1; +} + +static gint +find_contact_source_by_view (EContactStore *contact_store, + EBookClientView *client_view) +{ + GArray *array; + gint i; + + array = contact_store->priv->contact_sources; + + for (i = 0; i < array->len; i++) { + ContactSource *source; + + source = &g_array_index (array, ContactSource, i); + if (source->client_view == client_view || + source->client_view_pending == client_view) + return i; + } + + return -1; +} + +static gint +find_contact_source_by_offset (EContactStore *contact_store, + gint offset) +{ + GArray *array; + gint i; + + array = contact_store->priv->contact_sources; + + for (i = 0; i < array->len; i++) { + ContactSource *source; + + source = &g_array_index (array, ContactSource, i); + if (source->contacts->len > offset) + return i; + + offset -= source->contacts->len; + } + + return -1; +} + +static gint +find_contact_source_by_pointer (EContactStore *contact_store, + ContactSource *source) +{ + GArray *array; + gint i; + + array = contact_store->priv->contact_sources; + + i = ((gchar *) source - (gchar *) array->data) / sizeof (ContactSource); + + if (i < 0 || i >= array->len) + return -1; + + return i; +} + +static gint +get_contact_source_offset (EContactStore *contact_store, + gint contact_source_index) +{ + GArray *array; + gint offset = 0; + gint i; + + array = contact_store->priv->contact_sources; + + g_assert (contact_source_index < array->len); + + for (i = 0; i < contact_source_index; i++) { + ContactSource *source; + + source = &g_array_index (array, ContactSource, i); + offset += source->contacts->len; + } + + return offset; +} + +static gint +count_contacts (EContactStore *contact_store) +{ + GArray *array; + gint count = 0; + gint i; + + array = contact_store->priv->contact_sources; + + for (i = 0; i < array->len; i++) { + ContactSource *source; + + source = &g_array_index (array, ContactSource, i); + count += source->contacts->len; + } + + return count; +} + +static gint +find_contact_by_view_and_uid (EContactStore *contact_store, + EBookClientView *find_view, + const gchar *find_uid) +{ + GArray *array; + ContactSource *source; + GPtrArray *contacts; + gint source_index; + gint i; + + g_return_val_if_fail (find_uid != NULL, -1); + + source_index = find_contact_source_by_view (contact_store, find_view); + if (source_index < 0) + return -1; + + array = contact_store->priv->contact_sources; + source = &g_array_index (array, ContactSource, source_index); + + if (find_view == source->client_view) + contacts = source->contacts; /* Current view */ + else + contacts = source->contacts_pending; /* Pending view */ + + for (i = 0; i < contacts->len; i++) { + EContact *contact = g_ptr_array_index (contacts, i); + const gchar *uid = e_contact_get_const (contact, E_CONTACT_UID); + + if (uid && !strcmp (find_uid, uid)) + return i; + } + + return -1; +} + +static gint +find_contact_by_uid (EContactStore *contact_store, + const gchar *find_uid) +{ + GArray *array; + gint i; + + array = contact_store->priv->contact_sources; + + for (i = 0; i < array->len; i++) { + ContactSource *source = &g_array_index (array, ContactSource, i); + gint j; + + for (j = 0; j < source->contacts->len; j++) { + EContact *contact = g_ptr_array_index (source->contacts, j); + const gchar *uid = e_contact_get_const (contact, E_CONTACT_UID); + + if (!strcmp (find_uid, uid)) + return get_contact_source_offset (contact_store, i) + j; + } + } + + return -1; +} + +static EBookClient * +get_book_at_row (EContactStore *contact_store, + gint row) +{ + GArray *array; + ContactSource *source; + gint source_index; + + source_index = find_contact_source_by_offset (contact_store, row); + if (source_index < 0) + return NULL; + + array = contact_store->priv->contact_sources; + source = &g_array_index (array, ContactSource, source_index); + + return source->book_client; +} + +static EContact * +get_contact_at_row (EContactStore *contact_store, + gint row) +{ + GArray *array; + ContactSource *source; + gint source_index; + gint offset; + + source_index = find_contact_source_by_offset (contact_store, row); + if (source_index < 0) + return NULL; + + array = contact_store->priv->contact_sources; + source = &g_array_index (array, ContactSource, source_index); + offset = get_contact_source_offset (contact_store, source_index); + row -= offset; + + g_assert (row < source->contacts->len); + + return g_ptr_array_index (source->contacts, row); +} + +static gboolean +find_contact_source_details_by_view (EContactStore *contact_store, + EBookClientView *client_view, + ContactSource **contact_source, + gint *offset) +{ + GArray *array; + gint source_index; + + source_index = find_contact_source_by_view (contact_store, client_view); + if (source_index < 0) + return FALSE; + + array = contact_store->priv->contact_sources; + *contact_source = &g_array_index (array, ContactSource, source_index); + *offset = get_contact_source_offset (contact_store, source_index); + + return TRUE; +} + +/* ------------------------- * + * EBookView signal handlers * + * ------------------------- */ + +static void +view_contacts_added (EContactStore *contact_store, + const GSList *contacts, + EBookClientView *client_view) +{ + ContactSource *source; + gint offset; + const GSList *l; + + if (!find_contact_source_details_by_view (contact_store, client_view, &source, &offset)) { + g_warning ("EContactStore got 'contacts_added' signal from unknown EBookView!"); + return; + } + + for (l = contacts; l; l = g_slist_next (l)) { + EContact *contact = l->data; + + g_object_ref (contact); + + if (client_view == source->client_view) { + /* Current view */ + g_ptr_array_add (source->contacts, contact); + row_inserted (contact_store, offset + source->contacts->len - 1); + } else { + /* Pending view */ + g_ptr_array_add (source->contacts_pending, contact); + } + } +} + +static void +view_contacts_removed (EContactStore *contact_store, + const GSList *uids, + EBookClientView *client_view) +{ + ContactSource *source; + gint offset; + const GSList *l; + + if (!find_contact_source_details_by_view (contact_store, client_view, &source, &offset)) { + g_warning ("EContactStore got 'contacts_removed' signal from unknown EBookView!"); + return; + } + + for (l = uids; l; l = g_slist_next (l)) { + const gchar *uid = l->data; + gint n = find_contact_by_view_and_uid (contact_store, client_view, uid); + EContact *contact; + + if (n < 0) { + g_warning ("EContactStore got 'contacts_removed' on unknown contact!"); + continue; + } + + if (client_view == source->client_view) { + /* Current view */ + contact = g_ptr_array_index (source->contacts, n); + g_object_unref (contact); + g_ptr_array_remove_index (source->contacts, n); + row_deleted (contact_store, offset + n); + } else { + /* Pending view */ + contact = g_ptr_array_index (source->contacts_pending, n); + g_object_unref (contact); + g_ptr_array_remove_index (source->contacts_pending, n); + } + } +} + +static void +view_contacts_modified (EContactStore *contact_store, + const GSList *contacts, + EBookClientView *client_view) +{ + GPtrArray *cached_contacts; + ContactSource *source; + gint offset; + const GSList *l; + + if (!find_contact_source_details_by_view (contact_store, client_view, &source, &offset)) { + g_warning ("EContactStore got 'contacts_changed' signal from unknown EBookView!"); + return; + } + + if (client_view == source->client_view) + cached_contacts = source->contacts; + else + cached_contacts = source->contacts_pending; + + for (l = contacts; l; l = g_slist_next (l)) { + EContact *cached_contact; + EContact *contact = l->data; + const gchar *uid = e_contact_get_const (contact, E_CONTACT_UID); + gint n = find_contact_by_view_and_uid (contact_store, client_view, uid); + + if (n < 0) { + g_warning ("EContactStore got change notification on unknown contact!"); + continue; + } + + cached_contact = g_ptr_array_index (cached_contacts, n); + + /* Update cached contact */ + if (cached_contact != contact) { + g_object_unref (cached_contact); + cached_contacts->pdata[n] = g_object_ref (contact); + } + + /* Emit changes for current view only */ + if (client_view == source->client_view) + row_changed (contact_store, offset + n); + } +} + +static void +view_complete (EContactStore *contact_store, + const GError *error, + EBookClientView *client_view) +{ + ContactSource *source; + gint offset; + gint i; + + if (!find_contact_source_details_by_view (contact_store, client_view, &source, &offset)) { + g_warning ("EContactStore got 'complete' signal from unknown EBookClientView!"); + return; + } + + /* If current view finished, do nothing */ + if (client_view == source->client_view) { + stop_view (contact_store, source->client_view); + return; + } + + g_assert (client_view == source->client_view_pending); + + /* However, if it was a pending view, calculate and emit the differences between that + * and the current view, and move the pending view up to current. + * + * This is O(m * n), and can be sped up with a temporary hash table if needed. */ + + /* Deletions */ + for (i = 0; i < source->contacts->len; i++) { + EContact *old_contact = g_ptr_array_index (source->contacts, i); + const gchar *old_uid = e_contact_get_const (old_contact, E_CONTACT_UID); + gint result; + + result = find_contact_by_view_and_uid (contact_store, source->client_view_pending, old_uid); + if (result < 0) { + /* Contact is not in new view; removed */ + g_object_unref (old_contact); + g_ptr_array_remove_index (source->contacts, i); + row_deleted (contact_store, offset + i); + i--; /* Stay in place */ + } + } + + /* Insertions */ + for (i = 0; i < source->contacts_pending->len; i++) { + EContact *new_contact = g_ptr_array_index (source->contacts_pending, i); + const gchar *new_uid = e_contact_get_const (new_contact, E_CONTACT_UID); + gint result; + + result = find_contact_by_view_and_uid (contact_store, source->client_view, new_uid); + if (result < 0) { + /* Contact is not in old view; inserted */ + g_ptr_array_add (source->contacts, new_contact); + row_inserted (contact_store, offset + source->contacts->len - 1); + } else { + /* Contact already in old view; drop the new one */ + g_object_unref (new_contact); + } + } + + /* Move pending view up to current */ + stop_view (contact_store, source->client_view); + g_object_unref (source->client_view); + source->client_view = source->client_view_pending; + source->client_view_pending = NULL; + + /* Free array of pending contacts (members have been either moved or unreffed) */ + g_ptr_array_free (source->contacts_pending, TRUE); + source->contacts_pending = NULL; +} + +/* --------------------- * + * View/Query management * + * --------------------- */ + +static void +start_view (EContactStore *contact_store, + EBookClientView *view) +{ + g_signal_emit (contact_store, signals[START_CLIENT_VIEW], 0, view); + + g_signal_connect_swapped ( + view, "objects-added", + G_CALLBACK (view_contacts_added), contact_store); + g_signal_connect_swapped ( + view, "objects-removed", + G_CALLBACK (view_contacts_removed), contact_store); + g_signal_connect_swapped ( + view, "objects-modified", + G_CALLBACK (view_contacts_modified), contact_store); + g_signal_connect_swapped ( + view, "complete", + G_CALLBACK (view_complete), contact_store); + + e_book_client_view_start (view, NULL); +} + +static void +stop_view (EContactStore *contact_store, + EBookClientView *view) +{ + e_book_client_view_stop (view, NULL); + + g_signal_handlers_disconnect_matched ( + view, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, contact_store); + + g_signal_emit (contact_store, signals[STOP_CLIENT_VIEW], 0, view); +} + +static void +clear_contact_ptrarray (GPtrArray *contacts) +{ + gint i; + + for (i = 0; i < contacts->len; i++) { + EContact *contact = g_ptr_array_index (contacts, i); + g_object_unref (contact); + } + + g_ptr_array_set_size (contacts, 0); +} + +static void +free_contact_ptrarray (GPtrArray *contacts) +{ + clear_contact_ptrarray (contacts); + g_ptr_array_free (contacts, TRUE); +} + +static void +clear_contact_source (EContactStore *contact_store, + ContactSource *source) +{ + gint source_index; + gint offset; + + source_index = find_contact_source_by_pointer (contact_store, source); + g_assert (source_index >= 0); + + offset = get_contact_source_offset (contact_store, source_index); + g_assert (offset >= 0); + + /* Inform listeners that contacts went away */ + + if (source->contacts && source->contacts->len > 0) { + GtkTreePath *path = gtk_tree_path_new (); + gint i; + + gtk_tree_path_append_index (path, source->contacts->len); + + for (i = source->contacts->len - 1; i >= 0; i--) { + EContact *contact = g_ptr_array_index (source->contacts, i); + + g_object_unref (contact); + g_ptr_array_remove_index_fast (source->contacts, i); + + gtk_tree_path_prev (path); + gtk_tree_model_row_deleted (GTK_TREE_MODEL (contact_store), path); + } + + gtk_tree_path_free (path); + } + + /* Free main and pending views, clear cached contacts */ + + if (source->client_view) { + stop_view (contact_store, source->client_view); + g_object_unref (source->client_view); + + source->client_view = NULL; + } + + if (source->client_view_pending) { + stop_view (contact_store, source->client_view_pending); + g_object_unref (source->client_view_pending); + free_contact_ptrarray (source->contacts_pending); + + source->client_view_pending = NULL; + source->contacts_pending = NULL; + } +} + +static void +client_view_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EContactStore *contact_store = user_data; + gint source_idx; + EBookClient *book_client; + EBookClientView *client_view = NULL; + + g_return_if_fail (contact_store != NULL); + g_return_if_fail (source_object != NULL); + + book_client = E_BOOK_CLIENT (source_object); + g_return_if_fail (book_client != NULL); + + if (!e_book_client_get_view_finish (book_client, result, &client_view, NULL)) + client_view = NULL; + + source_idx = find_contact_source_by_client (contact_store, book_client); + if (source_idx >= 0) { + ContactSource *source; + + source = &g_array_index (contact_store->priv->contact_sources, ContactSource, source_idx); + + if (source->client_view) { + if (source->client_view_pending) { + stop_view (contact_store, source->client_view_pending); + g_object_unref (source->client_view_pending); + free_contact_ptrarray (source->contacts_pending); + } + + source->client_view_pending = client_view; + + if (source->client_view_pending) { + source->contacts_pending = g_ptr_array_new (); + start_view (contact_store, client_view); + } else { + source->contacts_pending = NULL; + } + } else { + source->client_view = client_view; + + if (source->client_view) { + start_view (contact_store, client_view); + } + } + } + + g_object_unref (contact_store); +} + +static void +query_contact_source (EContactStore *contact_store, + ContactSource *source) +{ + gboolean is_opened; + + g_assert (source->book_client != NULL); + + if (!contact_store->priv->query) { + clear_contact_source (contact_store, source); + return; + } + + is_opened = e_client_is_opened (E_CLIENT (source->book_client)); + + if (source->client_view) { + if (source->client_view_pending) { + stop_view (contact_store, source->client_view_pending); + g_object_unref (source->client_view_pending); + free_contact_ptrarray (source->contacts_pending); + source->client_view_pending = NULL; + source->contacts_pending = NULL; + } + } + + if (is_opened) { + gchar *query_str; + + query_str = e_book_query_to_string (contact_store->priv->query); + e_book_client_get_view (source->book_client, query_str, NULL, client_view_ready_cb, g_object_ref (contact_store)); + g_free (query_str); + } +} + +/* ----------------- * + * EContactStore API * + * ----------------- */ + +/** + * e_contact_store_get_client: + * @contact_store: an #EContactStore + * @iter: a #GtkTreeIter from @contact_store + * + * Gets the #EBookClient that provided the contact at @iter. + * + * Returns: An #EBookClient. + * + * Since: 3.2 + **/ +EBookClient * +e_contact_store_get_client (EContactStore *contact_store, + GtkTreeIter *iter) +{ + gint index; + + g_return_val_if_fail (E_IS_CONTACT_STORE (contact_store), NULL); + g_return_val_if_fail (ITER_IS_VALID (contact_store, iter), NULL); + + index = ITER_GET (iter); + + return get_book_at_row (contact_store, index); +} + +/** + * e_contact_store_get_contact: + * @contact_store: an #EContactStore + * @iter: a #GtkTreeIter from @contact_store + * + * Gets the #EContact at @iter. + * + * Returns: An #EContact. + **/ +EContact * +e_contact_store_get_contact (EContactStore *contact_store, + GtkTreeIter *iter) +{ + gint index; + + g_return_val_if_fail (E_IS_CONTACT_STORE (contact_store), NULL); + g_return_val_if_fail (ITER_IS_VALID (contact_store, iter), NULL); + + index = ITER_GET (iter); + + return get_contact_at_row (contact_store, index); +} + +/** + * e_contact_store_find_contact: + * @contact_store: an #EContactStore + * @uid: a unique contact identifier + * @iter: a destination #GtkTreeIter to set + * + * Sets @iter to point to the contact row matching @uid. + * + * Returns: %TRUE if the contact was found, and @iter was set. %FALSE otherwise. + **/ +gboolean +e_contact_store_find_contact (EContactStore *contact_store, + const gchar *uid, + GtkTreeIter *iter) +{ + gint index; + + g_return_val_if_fail (E_IS_CONTACT_STORE (contact_store), FALSE); + g_return_val_if_fail (uid != NULL, FALSE); + + index = find_contact_by_uid (contact_store, uid); + if (index < 0) + return FALSE; + + ITER_SET (contact_store, iter, index); + return TRUE; +} + +/** + * e_contact_store_get_clients: + * @contact_store: an #EContactStore + * + * Gets the list of book clients that provide contacts for @contact_store. + * + * Returns: A #GSList of pointers to #EBookClient. The caller owns the list, + * but not the book clients. + * + * Since: 3.2 + **/ +GSList * +e_contact_store_get_clients (EContactStore *contact_store) +{ + GArray *array; + GSList *client_list = NULL; + gint i; + + g_return_val_if_fail (E_IS_CONTACT_STORE (contact_store), NULL); + + array = contact_store->priv->contact_sources; + + for (i = 0; i < array->len; i++) { + ContactSource *source; + + source = &g_array_index (array, ContactSource, i); + client_list = g_slist_prepend (client_list, source->book_client); + } + + return client_list; +} + +/** + * e_contact_store_add_client: + * @contact_store: an #EContactStore + * @book_client: an #EBookClient + * + * Adds @book_client to the list of book clients that provide contacts for @contact_store. + * The @contact_store adds a reference to @book_client, if added. + * + * Since: 3.2 + **/ +void +e_contact_store_add_client (EContactStore *contact_store, + EBookClient *book_client) +{ + GArray *array; + ContactSource source; + ContactSource *indexed_source; + + g_return_if_fail (E_IS_CONTACT_STORE (contact_store)); + g_return_if_fail (E_IS_BOOK_CLIENT (book_client)); + + if (find_contact_source_by_client (contact_store, book_client) >= 0) { + g_warning ("Same book client added more than once to EContactStore!"); + return; + } + + array = contact_store->priv->contact_sources; + + memset (&source, 0, sizeof (ContactSource)); + source.book_client = g_object_ref (book_client); + source.contacts = g_ptr_array_new (); + g_array_append_val (array, source); + + indexed_source = &g_array_index (array, ContactSource, array->len - 1); + + query_contact_source (contact_store, indexed_source); +} + +/** + * e_contact_store_remove_client: + * @contact_store: an #EContactStore + * @book_client: an #EBookClient + * + * Removes @book from the list of book clients that provide contacts for @contact_store. + * + * Since: 3.2 + **/ +void +e_contact_store_remove_client (EContactStore *contact_store, + EBookClient *book_client) +{ + GArray *array; + ContactSource *source; + gint source_index; + + g_return_if_fail (E_IS_CONTACT_STORE (contact_store)); + g_return_if_fail (E_IS_BOOK_CLIENT (book_client)); + + source_index = find_contact_source_by_client (contact_store, book_client); + if (source_index < 0) { + g_warning ("Tried to remove unknown book client from EContactStore!"); + return; + } + + array = contact_store->priv->contact_sources; + + source = &g_array_index (array, ContactSource, source_index); + clear_contact_source (contact_store, source); + free_contact_ptrarray (source->contacts); + g_object_unref (book_client); + + g_array_remove_index (array, source_index); /* Preserve order */ +} + +/** + * e_contact_store_set_query: + * @contact_store: an #EContactStore + * @book_query: an #EBookQuery + * + * Sets @book_query to be the query used to fetch contacts from the books + * assigned to @contact_store. + **/ +void +e_contact_store_set_query (EContactStore *contact_store, + EBookQuery *book_query) +{ + GArray *array; + gint i; + + g_return_if_fail (E_IS_CONTACT_STORE (contact_store)); + + if (book_query == contact_store->priv->query) + return; + + if (contact_store->priv->query) + e_book_query_unref (contact_store->priv->query); + + contact_store->priv->query = book_query; + if (book_query) + e_book_query_ref (book_query); + + /* Query books */ + array = contact_store->priv->contact_sources; + for (i = 0; i < array->len; i++) { + ContactSource *contact_source; + + contact_source = &g_array_index (array, ContactSource, i); + query_contact_source (contact_store, contact_source); + } +} + +/** + * e_contact_store_peek_query: + * @contact_store: an #EContactStore + * + * Gets the query that's being used to fetch contacts from the books + * assigned to @contact_store. + * + * Returns: The #EBookQuery being used. + **/ +EBookQuery * +e_contact_store_peek_query (EContactStore *contact_store) +{ + g_return_val_if_fail (E_IS_CONTACT_STORE (contact_store), NULL); + + return contact_store->priv->query; +} + +/* ---------------- * + * GtkTreeModel API * + * ---------------- */ + +static GtkTreeModelFlags +e_contact_store_get_flags (GtkTreeModel *tree_model) +{ + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), 0); + + return GTK_TREE_MODEL_LIST_ONLY; +} + +static gint +e_contact_store_get_n_columns (GtkTreeModel *tree_model) +{ + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), 0); + + return E_CONTACT_FIELD_LAST; +} + +static GType +get_column_type (EContactStore *contact_store, + gint column) +{ + const gchar *field_name; + GObjectClass *contact_class; + GParamSpec *param_spec; + GType value_type; + + /* Silently suppress requests for columns lower than the first EContactField. + * GtkTreeView automatically queries the type of all columns up to the maximum + * provided, and we have to return a valid value type, so let it be a generic + * pointer. */ + if (column < E_CONTACT_FIELD_FIRST) { + return G_TYPE_POINTER; + } + + field_name = e_contact_field_name (column); + contact_class = g_type_class_ref (E_TYPE_CONTACT); + param_spec = g_object_class_find_property (contact_class, field_name); + value_type = G_PARAM_SPEC_VALUE_TYPE (param_spec); + g_type_class_unref (contact_class); + + return value_type; +} + +static GType +e_contact_store_get_column_type (GtkTreeModel *tree_model, + gint index) +{ + EContactStore *contact_store = E_CONTACT_STORE (tree_model); + + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), G_TYPE_INVALID); + g_return_val_if_fail (index >= 0 && index < E_CONTACT_FIELD_LAST, G_TYPE_INVALID); + + return get_column_type (contact_store, index); +} + +static gboolean +e_contact_store_get_iter (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreePath *path) +{ + EContactStore *contact_store = E_CONTACT_STORE (tree_model); + gint index; + + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), FALSE); + g_return_val_if_fail (gtk_tree_path_get_depth (path) > 0, FALSE); + + index = gtk_tree_path_get_indices (path)[0]; + if (index >= count_contacts (contact_store)) + return FALSE; + + ITER_SET (contact_store, iter, index); + return TRUE; +} + +static GtkTreePath * +e_contact_store_get_path (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + EContactStore *contact_store = E_CONTACT_STORE (tree_model); + GtkTreePath *path; + gint index; + + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), NULL); + g_return_val_if_fail (ITER_IS_VALID (contact_store, iter), NULL); + + index = ITER_GET (iter); + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, index); + + return path; +} + +static gboolean +e_contact_store_iter_next (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + EContactStore *contact_store = E_CONTACT_STORE (tree_model); + gint index; + + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), FALSE); + g_return_val_if_fail (ITER_IS_VALID (contact_store, iter), FALSE); + + index = ITER_GET (iter); + + if (index + 1 < count_contacts (contact_store)) { + ITER_SET (contact_store, iter, index + 1); + return TRUE; + } + + return FALSE; +} + +static gboolean +e_contact_store_iter_children (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent) +{ + EContactStore *contact_store = E_CONTACT_STORE (tree_model); + + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), FALSE); + + /* This is a list, nodes have no children. */ + if (parent) + return FALSE; + + /* But if parent == NULL we return the list itself as children of the root. */ + if (count_contacts (contact_store) <= 0) + return FALSE; + + ITER_SET (contact_store, iter, 0); + return TRUE; +} + +static gboolean +e_contact_store_iter_has_child (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), FALSE); + + if (iter == NULL) + return TRUE; + + return FALSE; +} + +static gint +e_contact_store_iter_n_children (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + EContactStore *contact_store = E_CONTACT_STORE (tree_model); + + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), -1); + + if (iter == NULL) + return count_contacts (contact_store); + + g_return_val_if_fail (ITER_IS_VALID (contact_store, iter), -1); + return 0; +} + +static gboolean +e_contact_store_iter_nth_child (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent, + gint n) +{ + EContactStore *contact_store = E_CONTACT_STORE (tree_model); + + g_return_val_if_fail (E_IS_CONTACT_STORE (tree_model), FALSE); + + if (parent) + return FALSE; + + if (n < count_contacts (contact_store)) { + ITER_SET (contact_store, iter, n); + return TRUE; + } + + return FALSE; +} + +static gboolean +e_contact_store_iter_parent (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *child) +{ + return FALSE; +} + +static void +e_contact_store_get_value (GtkTreeModel *tree_model, + GtkTreeIter *iter, + gint column, + GValue *value) +{ + EContactStore *contact_store = E_CONTACT_STORE (tree_model); + EContact *contact; + const gchar *field_name; + gint row; + + g_return_if_fail (E_IS_CONTACT_STORE (tree_model)); + g_return_if_fail (column < E_CONTACT_FIELD_LAST); + g_return_if_fail (ITER_IS_VALID (contact_store, iter)); + + g_value_init (value, get_column_type (contact_store, column)); + + row = ITER_GET (iter); + contact = get_contact_at_row (contact_store, row); + if (!contact || column < E_CONTACT_FIELD_FIRST) + return; + + field_name = e_contact_field_name (column); + g_object_get_property (G_OBJECT (contact), field_name, value); +} diff --git a/e-util/e-contact-store.h b/e-util/e-contact-store.h new file mode 100644 index 0000000000..c0754afab0 --- /dev/null +++ b/e-util/e-contact-store.h @@ -0,0 +1,94 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-contact-store.h - Contacts store with GtkTreeModel interface. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_CONTACT_STORE_H +#define E_CONTACT_STORE_H + +#include <gtk/gtk.h> +#include <libebook/libebook.h> + +/* Standard GObject macros */ +#define E_TYPE_CONTACT_STORE \ + (e_contact_store_get_type ()) +#define E_CONTACT_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CONTACT_STORE, EContactStore)) +#define E_CONTACT_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CONTACT_STORE, EContactStoreClass)) +#define E_IS_CONTACT_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CONTACT_STORE)) +#define E_IS_CONTACT_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CONTACT_STORE)) +#define E_CONTACT_STORE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CONTACT_STORE, EContactStoreClass)) + +G_BEGIN_DECLS + +typedef struct _EContactStore EContactStore; +typedef struct _EContactStoreClass EContactStoreClass; +typedef struct _EContactStorePrivate EContactStorePrivate; + +struct _EContactStore { + GObject parent; + EContactStorePrivate *priv; +}; + +struct _EContactStoreClass { + GObjectClass parent_class; + + /* signals */ + void (*start_client_view) (EContactStore *contact_store, EBookClientView *client_view); + void (*stop_client_view) (EContactStore *contact_store, EBookClientView *client_view); +}; + +GType e_contact_store_get_type (void); +EContactStore * e_contact_store_new (void); + +EBookClient * e_contact_store_get_client (EContactStore *contact_store, + GtkTreeIter *iter); +EContact * e_contact_store_get_contact (EContactStore *contact_store, + GtkTreeIter *iter); +gboolean e_contact_store_find_contact (EContactStore *contact_store, + const gchar *uid, + GtkTreeIter *iter); + +/* Returns a shallow copy; free the list when done, but don't unref elements */ +GSList * e_contact_store_get_clients (EContactStore *contact_store); +void e_contact_store_add_client (EContactStore *contact_store, + EBookClient *book_client); +void e_contact_store_remove_client (EContactStore *contact_store, + EBookClient *book_client); +void e_contact_store_set_query (EContactStore *contact_store, + EBookQuery *book_query); +EBookQuery * e_contact_store_peek_query (EContactStore *contact_store); + +G_END_DECLS + +#endif /* E_CONTACT_STORE_H */ diff --git a/e-util/e-dateedit.c b/e-util/e-dateedit.c new file mode 100644 index 0000000000..ab6085f44b --- /dev/null +++ b/e-util/e-dateedit.c @@ -0,0 +1,2497 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +/* + * EDateEdit - a widget based on GnomeDateEdit to provide a date & optional + * time field with popups for entering a date. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-dateedit.h" + +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <gdk/gdkkeysyms.h> +#include <atk/atkrelation.h> +#include <atk/atkrelationset.h> +#include <glib/gi18n.h> + +#include <libebackend/libebackend.h> + +#include "e-calendar.h" + +#define E_DATE_EDIT_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_DATE_EDIT, EDateEditPrivate)) + +struct _EDateEditPrivate { + GtkWidget *date_entry; + GtkWidget *date_button; + + GtkWidget *space; + + GtkWidget *time_combo; + + GtkWidget *cal_popup; + GtkWidget *calendar; + GtkWidget *now_button; + GtkWidget *today_button; + GtkWidget *none_button; /* This will only be visible if a + * 'None' date/time is permitted. */ + + GdkDevice *grabbed_keyboard; + GdkDevice *grabbed_pointer; + + gboolean show_date; + gboolean show_time; + gboolean use_24_hour_format; + + /* This is TRUE if we want to make the time field insensitive rather + * than hide it when set_show_time() is called. */ + gboolean make_time_insensitive; + + /* This is the range of hours we show in the time popup. */ + gint lower_hour; + gint upper_hour; + + /* This indicates whether the last date committed was invalid. + * (A date is committed by hitting Return, moving the keyboard focus, + * or selecting a date in the popup). Note that this only indicates + * that the date couldn't be parsed. A date set to 'None' is valid + * here, though e_date_edit_date_is_valid() will return FALSE if an + * empty date isn't actually permitted. */ + gboolean date_is_valid; + + /* This is the last valid date which was set. If the date was set to + * 'None' or empty, date_set_to_none will be TRUE and the other fields + * are undefined, so don't use them. */ + gboolean date_set_to_none; + gint year; + gint month; + gint day; + + /* This indicates whether the last time committed was invalid. + * (A time is committed by hitting Return, moving the keyboard focus, + * or selecting a time in the popup). Note that this only indicates + * that the time couldn't be parsed. An empty/None time is valid + * here, though e_date_edit_time_is_valid() will return FALSE if an + * empty time isn't actually permitted. */ + gboolean time_is_valid; + + /* This is the last valid time which was set. If the time was set to + * 'None' or empty, time_set_to_none will be TRUE and the other fields + * are undefined, so don't use them. */ + gboolean time_set_to_none; + gint hour; + gint minute; + + EDateEditGetTimeCallback time_callback; + gpointer time_callback_data; + GDestroyNotify time_callback_destroy; + + gboolean twodigit_year_can_future; + + /* set to TRUE when the date has been changed by typing to the entry */ + gboolean has_been_changed; + + gboolean allow_no_date_set; +}; + +enum { + PROP_0, + PROP_ALLOW_NO_DATE_SET, + PROP_SHOW_DATE, + PROP_SHOW_TIME, + PROP_SHOW_WEEK_NUMBERS, + PROP_USE_24_HOUR_FORMAT, + PROP_WEEK_START_DAY, + PROP_TWODIGIT_YEAR_CAN_FUTURE, + PROP_SET_NONE +}; + +enum { + CHANGED, + LAST_SIGNAL +}; + +static void create_children (EDateEdit *dedit); +static gboolean e_date_edit_mnemonic_activate (GtkWidget *widget, + gboolean group_cycling); +static void e_date_edit_grab_focus (GtkWidget *widget); + +static gint on_date_entry_key_press (GtkWidget *widget, + GdkEvent *key_event, + EDateEdit *dedit); +static gint on_date_entry_key_release (GtkWidget *widget, + GdkEvent *key_event, + EDateEdit *dedit); +static void on_date_button_clicked (GtkWidget *widget, + EDateEdit *dedit); +static void e_date_edit_show_date_popup (EDateEdit *dedit, + GdkEvent *event); +static void position_date_popup (EDateEdit *dedit); +static void on_date_popup_none_button_clicked (GtkWidget *button, + EDateEdit *dedit); +static void on_date_popup_today_button_clicked (GtkWidget *button, + EDateEdit *dedit); +static void on_date_popup_now_button_clicked (GtkWidget *button, + EDateEdit *dedit); +static gint on_date_popup_delete_event (GtkWidget *widget, + EDateEdit *dedit); +static gint on_date_popup_key_press (GtkWidget *widget, + GdkEventKey *event, + EDateEdit *dedit); +static gint on_date_popup_button_press (GtkWidget *widget, + GdkEvent *button_event, + gpointer data); +static void on_date_popup_date_selected (ECalendarItem *calitem, + EDateEdit *dedit); +static void hide_date_popup (EDateEdit *dedit); +static void rebuild_time_popup (EDateEdit *dedit); +static gboolean field_set_to_none (const gchar *text); +static gboolean e_date_edit_parse_date (EDateEdit *dedit, + const gchar *date_text, + struct tm *date_tm); +static gboolean e_date_edit_parse_time (EDateEdit *dedit, + const gchar *time_text, + struct tm *time_tm); +static void on_date_edit_time_selected (GtkComboBox *combo, + EDateEdit *dedit); +static gint on_time_entry_key_press (GtkWidget *widget, + GdkEvent *key_event, + EDateEdit *dedit); +static gint on_time_entry_key_release (GtkWidget *widget, + GdkEvent *key_event, + EDateEdit *dedit); +static gint on_date_entry_focus_out (GtkEntry *entry, + GdkEventFocus *event, + EDateEdit *dedit); +static gint on_time_entry_focus_out (GtkEntry *entry, + GdkEventFocus *event, + EDateEdit *dedit); +static void e_date_edit_update_date_entry (EDateEdit *dedit); +static void e_date_edit_update_time_entry (EDateEdit *dedit); +static void e_date_edit_update_time_combo_state (EDateEdit *dedit); +static void e_date_edit_check_date_changed (EDateEdit *dedit); +static void e_date_edit_check_time_changed (EDateEdit *dedit); +static gboolean e_date_edit_set_date_internal (EDateEdit *dedit, + gboolean valid, + gboolean none, + gint year, + gint month, + gint day); +static gboolean e_date_edit_set_time_internal (EDateEdit *dedit, + gboolean valid, + gboolean none, + gint hour, + gint minute); + +static gint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE_WITH_CODE ( + EDateEdit, + e_date_edit, + GTK_TYPE_HBOX, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +static void +date_edit_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ALLOW_NO_DATE_SET: + e_date_edit_set_allow_no_date_set ( + E_DATE_EDIT (object), + g_value_get_boolean (value)); + return; + + case PROP_SHOW_DATE: + e_date_edit_set_show_date ( + E_DATE_EDIT (object), + g_value_get_boolean (value)); + return; + + case PROP_SHOW_TIME: + e_date_edit_set_show_time ( + E_DATE_EDIT (object), + g_value_get_boolean (value)); + return; + + case PROP_SHOW_WEEK_NUMBERS: + e_date_edit_set_show_week_numbers ( + E_DATE_EDIT (object), + g_value_get_boolean (value)); + return; + + case PROP_USE_24_HOUR_FORMAT: + e_date_edit_set_use_24_hour_format ( + E_DATE_EDIT (object), + g_value_get_boolean (value)); + return; + + case PROP_WEEK_START_DAY: + e_date_edit_set_week_start_day ( + E_DATE_EDIT (object), + g_value_get_int (value)); + return; + + case PROP_TWODIGIT_YEAR_CAN_FUTURE: + e_date_edit_set_twodigit_year_can_future ( + E_DATE_EDIT (object), + g_value_get_boolean (value)); + return; + + case PROP_SET_NONE: + if (g_value_get_boolean (value)) + e_date_edit_set_time (E_DATE_EDIT (object), -1); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +date_edit_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ALLOW_NO_DATE_SET: + g_value_set_boolean ( + value, e_date_edit_get_allow_no_date_set ( + E_DATE_EDIT (object))); + return; + + case PROP_SHOW_DATE: + g_value_set_boolean ( + value, e_date_edit_get_show_date ( + E_DATE_EDIT (object))); + return; + + case PROP_SHOW_TIME: + g_value_set_boolean ( + value, e_date_edit_get_show_time ( + E_DATE_EDIT (object))); + return; + + case PROP_SHOW_WEEK_NUMBERS: + g_value_set_boolean ( + value, e_date_edit_get_show_week_numbers ( + E_DATE_EDIT (object))); + return; + + case PROP_USE_24_HOUR_FORMAT: + g_value_set_boolean ( + value, e_date_edit_get_use_24_hour_format ( + E_DATE_EDIT (object))); + return; + + case PROP_WEEK_START_DAY: + g_value_set_int ( + value, e_date_edit_get_week_start_day ( + E_DATE_EDIT (object))); + return; + + case PROP_TWODIGIT_YEAR_CAN_FUTURE: + g_value_set_boolean ( + value, e_date_edit_get_twodigit_year_can_future ( + E_DATE_EDIT (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +date_edit_dispose (GObject *object) +{ + EDateEdit *dedit; + + dedit = E_DATE_EDIT (object); + + e_date_edit_set_get_time_callback (dedit, NULL, NULL, NULL); + + if (dedit->priv->cal_popup != NULL) { + gtk_widget_destroy (dedit->priv->cal_popup); + dedit->priv->cal_popup = NULL; + } + + if (dedit->priv->grabbed_keyboard != NULL) { + gdk_device_ungrab ( + dedit->priv->grabbed_keyboard, + GDK_CURRENT_TIME); + g_object_unref (dedit->priv->grabbed_keyboard); + dedit->priv->grabbed_keyboard = NULL; + } + + if (dedit->priv->grabbed_pointer != NULL) { + gdk_device_ungrab ( + dedit->priv->grabbed_pointer, + GDK_CURRENT_TIME); + g_object_unref (dedit->priv->grabbed_pointer); + dedit->priv->grabbed_pointer = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_date_edit_parent_class)->dispose (object); +} + +static void +e_date_edit_class_init (EDateEditClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EDateEditPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = date_edit_set_property; + object_class->get_property = date_edit_get_property; + object_class->dispose = date_edit_dispose; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->mnemonic_activate = e_date_edit_mnemonic_activate; + widget_class->grab_focus = e_date_edit_grab_focus; + + g_object_class_install_property ( + object_class, + PROP_ALLOW_NO_DATE_SET, + g_param_spec_boolean ( + "allow-no-date-set", + "Allow No Date Set", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_DATE, + g_param_spec_boolean ( + "show-date", + "Show Date", + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_TIME, + g_param_spec_boolean ( + "show-time", + "Show Time", + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_WEEK_NUMBERS, + g_param_spec_boolean ( + "show-week-numbers", + "Show Week Numbers", + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_USE_24_HOUR_FORMAT, + g_param_spec_boolean ( + "use-24-hour-format", + "Use 24-Hour Format", + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WEEK_START_DAY, + g_param_spec_int ( + "week-start-day", + "Week Start Day", + NULL, + 0, /* Monday */ + 6, /* Sunday */ + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_TWODIGIT_YEAR_CAN_FUTURE, + g_param_spec_boolean ( + "twodigit-year-can-future", + "Two-digit year can be treated as future", + NULL, + TRUE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SET_NONE, + g_param_spec_boolean ( + "set-none", + "Sets None as selected date", + NULL, + FALSE, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); + + signals[CHANGED] = g_signal_new ( + "changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (EDateEditClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_date_edit_init (EDateEdit *dedit) +{ + dedit->priv = E_DATE_EDIT_GET_PRIVATE (dedit); + + dedit->priv->show_date = TRUE; + dedit->priv->show_time = TRUE; + dedit->priv->use_24_hour_format = TRUE; + + dedit->priv->make_time_insensitive = FALSE; + + dedit->priv->lower_hour = 0; + dedit->priv->upper_hour = 24; + + dedit->priv->date_is_valid = TRUE; + dedit->priv->date_set_to_none = TRUE; + dedit->priv->time_is_valid = TRUE; + dedit->priv->time_set_to_none = TRUE; + dedit->priv->time_callback = NULL; + dedit->priv->time_callback_data = NULL; + dedit->priv->time_callback_destroy = NULL; + + dedit->priv->twodigit_year_can_future = TRUE; + dedit->priv->has_been_changed = FALSE; + + create_children (dedit); + + /* Set it to the current time. */ + e_date_edit_set_time (dedit, 0); + + e_extensible_load_extensions (E_EXTENSIBLE (dedit)); +} + +/** + * e_date_edit_new: + * + * Description: Creates a new #EDateEdit widget which can be used + * to provide an easy to use way for entering dates and times. + * + * Returns: a new #EDateEdit widget. + */ +GtkWidget * +e_date_edit_new (void) +{ + EDateEdit *dedit; + AtkObject *a11y; + + dedit = g_object_new (E_TYPE_DATE_EDIT, NULL); + a11y = gtk_widget_get_accessible (GTK_WIDGET (dedit)); + atk_object_set_name (a11y, _("Date and Time")); + + return GTK_WIDGET (dedit); +} + +static void +create_children (EDateEdit *dedit) +{ + EDateEditPrivate *priv; + ECalendar *calendar; + GtkWidget *frame, *arrow; + GtkWidget *vbox, *bbox; + GtkWidget *child; + AtkObject *a11y; + GtkListStore *time_store; + GList *cells; + GtkCssProvider *css_provider; + GtkStyleContext *style_context; + const gchar *css; + GError *error = NULL; + + priv = dedit->priv; + + priv->date_entry = gtk_entry_new (); + a11y = gtk_widget_get_accessible (priv->date_entry); + atk_object_set_description (a11y, _("Text entry to input date")); + atk_object_set_name (a11y, _("Date")); + gtk_box_pack_start (GTK_BOX (dedit), priv->date_entry, FALSE, TRUE, 0); + gtk_widget_set_size_request (priv->date_entry, 100, -1); + + g_signal_connect ( + priv->date_entry, "key_press_event", + G_CALLBACK (on_date_entry_key_press), dedit); + g_signal_connect ( + priv->date_entry, "key_release_event", + G_CALLBACK (on_date_entry_key_release), dedit); + g_signal_connect_after ( + priv->date_entry, "focus_out_event", + G_CALLBACK (on_date_entry_focus_out), dedit); + + priv->date_button = gtk_button_new (); + g_signal_connect ( + priv->date_button, "clicked", + G_CALLBACK (on_date_button_clicked), dedit); + gtk_box_pack_start ( + GTK_BOX (dedit), priv->date_button, + FALSE, FALSE, 0); + a11y = gtk_widget_get_accessible (priv->date_button); + atk_object_set_description (a11y, _("Click this button to show a calendar")); + atk_object_set_name (a11y, _("Date")); + + arrow = gtk_arrow_new (GTK_ARROW_DOWN, GTK_SHADOW_NONE); + gtk_container_add (GTK_CONTAINER (priv->date_button), arrow); + gtk_widget_show (arrow); + + if (priv->show_date) { + gtk_widget_show (priv->date_entry); + gtk_widget_show (priv->date_button); + } + + /* This is just to create a space between the date & time parts. */ + priv->space = gtk_drawing_area_new (); + gtk_box_pack_start (GTK_BOX (dedit), priv->space, FALSE, FALSE, 2); + + time_store = gtk_list_store_new (1, G_TYPE_STRING); + priv->time_combo = gtk_combo_box_new_with_model_and_entry ( + GTK_TREE_MODEL (time_store)); + gtk_combo_box_set_entry_text_column (GTK_COMBO_BOX (priv->time_combo), 0); + g_object_unref (time_store); + + css_provider = gtk_css_provider_new (); + css = "GtkComboBox { -GtkComboBox-appears-as-list: 1; }"; + gtk_css_provider_load_from_data (css_provider, css, -1, &error); + style_context = gtk_widget_get_style_context (priv->time_combo); + if (error == NULL) { + gtk_style_context_add_provider ( + style_context, + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } else { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_clear_error (&error); + } + g_object_unref (css_provider); + + child = gtk_bin_get_child (GTK_BIN (priv->time_combo)); + + /* We need to make sure labels are right-aligned, since we want + * digits to line up, and with a nonproportional font, the width + * of a space != width of a digit. Technically, only 12-hour + * format needs this, but we do it always, for consistency. */ + g_object_set (child, "xalign", 1.0, NULL); + cells = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (priv->time_combo)); + if (cells) { + g_object_set (GTK_CELL_RENDERER (cells->data), "xalign", 1.0, NULL); + g_list_free (cells); + } + + gtk_box_pack_start (GTK_BOX (dedit), priv->time_combo, FALSE, TRUE, 0); + gtk_widget_set_size_request (priv->time_combo, 110, -1); + rebuild_time_popup (dedit); + a11y = gtk_widget_get_accessible (priv->time_combo); + atk_object_set_description (a11y, _("Drop-down combination box to select time")); + atk_object_set_name (a11y, _("Time")); + + g_signal_connect ( + child, "key_press_event", + G_CALLBACK (on_time_entry_key_press), dedit); + g_signal_connect ( + child, "key_release_event", + G_CALLBACK (on_time_entry_key_release), dedit); + g_signal_connect_after ( + child, "focus_out_event", + G_CALLBACK (on_time_entry_focus_out), dedit); + g_signal_connect_after ( + priv->time_combo, "changed", + G_CALLBACK (on_date_edit_time_selected), dedit); + + if (priv->show_time || priv->make_time_insensitive) + gtk_widget_show (priv->time_combo); + + if (!priv->show_time && priv->make_time_insensitive) + gtk_widget_set_sensitive (priv->time_combo, FALSE); + + if (priv->show_date + && (priv->show_time || priv->make_time_insensitive)) + gtk_widget_show (priv->space); + + priv->cal_popup = gtk_window_new (GTK_WINDOW_POPUP); + gtk_window_set_type_hint ( + GTK_WINDOW (priv->cal_popup), + GDK_WINDOW_TYPE_HINT_COMBO); + gtk_widget_set_events ( + priv->cal_popup, + gtk_widget_get_events (priv->cal_popup) + | GDK_KEY_PRESS_MASK); + g_signal_connect ( + priv->cal_popup, "delete_event", + G_CALLBACK (on_date_popup_delete_event), dedit); + g_signal_connect ( + priv->cal_popup, "key_press_event", + G_CALLBACK (on_date_popup_key_press), dedit); + g_signal_connect ( + priv->cal_popup, "button_press_event", + G_CALLBACK (on_date_popup_button_press), dedit); + gtk_window_set_resizable (GTK_WINDOW (priv->cal_popup), TRUE); + + frame = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_OUT); + gtk_container_add (GTK_CONTAINER (priv->cal_popup), frame); + gtk_widget_show (frame); + + vbox = gtk_vbox_new (FALSE, 0); + gtk_container_add (GTK_CONTAINER (frame), vbox); + gtk_widget_show (vbox); + + priv->calendar = e_calendar_new (); + calendar = E_CALENDAR (priv->calendar); + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (calendar->calitem), + "maximum_days_selected", 1, + "move_selection_when_moving", FALSE, + NULL); + + g_signal_connect ( + calendar->calitem, "selection_changed", + G_CALLBACK (on_date_popup_date_selected), dedit); + + gtk_box_pack_start (GTK_BOX (vbox), priv->calendar, FALSE, FALSE, 0); + gtk_widget_show (priv->calendar); + + bbox = gtk_hbutton_box_new (); + gtk_container_set_border_width (GTK_CONTAINER (bbox), 4); + gtk_box_set_spacing (GTK_BOX (bbox), 2); + gtk_box_pack_start (GTK_BOX (vbox), bbox, FALSE, FALSE, 0); + gtk_widget_show (bbox); + + priv->now_button = gtk_button_new_with_mnemonic (_("No_w")); + gtk_container_add (GTK_CONTAINER (bbox), priv->now_button); + gtk_widget_show (priv->now_button); + g_signal_connect ( + priv->now_button, "clicked", + G_CALLBACK (on_date_popup_now_button_clicked), dedit); + + priv->today_button = gtk_button_new_with_mnemonic (_("_Today")); + gtk_container_add (GTK_CONTAINER (bbox), priv->today_button); + gtk_widget_show (priv->today_button); + g_signal_connect ( + priv->today_button, "clicked", + G_CALLBACK (on_date_popup_today_button_clicked), dedit); + + /* Note that we don't show this here, since by default a 'None' date + * is not permitted. */ + priv->none_button = gtk_button_new_with_mnemonic (_("_None")); + gtk_container_add (GTK_CONTAINER (bbox), priv->none_button); + g_signal_connect ( + priv->none_button, "clicked", + G_CALLBACK (on_date_popup_none_button_clicked), dedit); + g_object_bind_property ( + dedit, "allow-no-date-set", + priv->none_button, "visible", + G_BINDING_SYNC_CREATE); +} + +/* GtkWidget::mnemonic_activate() handler for the EDateEdit */ +static gboolean +e_date_edit_mnemonic_activate (GtkWidget *widget, + gboolean group_cycling) +{ + e_date_edit_grab_focus (widget); + return TRUE; +} + +/* Grab_focus handler for the EDateEdit. If the date field is being shown, we + * grab the focus to that, otherwise we grab it to the time field. */ +static void +e_date_edit_grab_focus (GtkWidget *widget) +{ + EDateEdit *dedit; + GtkWidget *child; + + g_return_if_fail (E_IS_DATE_EDIT (widget)); + + dedit = E_DATE_EDIT (widget); + child = gtk_bin_get_child (GTK_BIN (dedit->priv->time_combo)); + + if (dedit->priv->show_date) + gtk_widget_grab_focus (dedit->priv->date_entry); + else + gtk_widget_grab_focus (child); +} + +/** + * e_date_edit_set_editable: + * @dedit: an #EDateEdit widget. + * @editable: whether or not the widget should accept edits. + * + * Allows the programmer to disallow editing (and the popping up of + * the calendar widget), while still allowing the user to select the + * date from the GtkEntry. + */ +void +e_date_edit_set_editable (EDateEdit *dedit, + gboolean editable) +{ + EDateEditPrivate *priv; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + priv = dedit->priv; + + gtk_editable_set_editable (GTK_EDITABLE (priv->date_entry), editable); + gtk_widget_set_sensitive (priv->date_button, editable); +} + +/** + * e_date_edit_get_time: + * @dedit: an #EDateEdit widget. + * @the_time: returns the last valid time entered. + * @Returns: the last valid time entered, or -1 if the time is not set. + * + * Returns the last valid time entered. If empty times are valid, by calling + * e_date_edit_set_allow_no_date_set(), then it may return -1. + * + * Note that the last time entered may actually have been invalid. You can + * check this with e_date_edit_time_is_valid(). + */ +time_t +e_date_edit_get_time (EDateEdit *dedit) +{ + EDateEditPrivate *priv; + struct tm tmp_tm = { 0 }; + + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), -1); + + priv = dedit->priv; + + /* Try to parse any new value now. */ + e_date_edit_check_date_changed (dedit); + e_date_edit_check_time_changed (dedit); + + if (priv->date_set_to_none) + return -1; + + tmp_tm.tm_year = priv->year; + tmp_tm.tm_mon = priv->month; + tmp_tm.tm_mday = priv->day; + + if (!priv->show_time || priv->time_set_to_none) { + tmp_tm.tm_hour = 0; + tmp_tm.tm_min = 0; + } else { + tmp_tm.tm_hour = priv->hour; + tmp_tm.tm_min = priv->minute; + } + tmp_tm.tm_sec = 0; + tmp_tm.tm_isdst = -1; + + return mktime (&tmp_tm); +} + +/** + * e_date_edit_set_time: + * @dedit: the EDateEdit widget + * @the_time: The time and date that should be set on the widget + * + * Description: Changes the displayed date and time in the EDateEdit + * widget to be the one represented by @the_time. If @the_time is 0 + * then current time is used. If it is -1, then the date is set to None. + * + * Note that the time is converted to local time using the Unix timezone, + * so if you are using your own timezones then you should use + * e_date_edit_set_date() and e_date_edit_set_time_of_day() instead. + */ +void +e_date_edit_set_time (EDateEdit *dedit, + time_t the_time) +{ + EDateEditPrivate *priv; + struct tm tmp_tm; + gboolean date_changed = FALSE, time_changed = FALSE; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + priv = dedit->priv; + + if (the_time == -1) { + date_changed = e_date_edit_set_date_internal ( + dedit, TRUE, + TRUE, 0, 0, 0); + time_changed = e_date_edit_set_time_internal ( + dedit, TRUE, + TRUE, 0, 0); + } else { + if (the_time == 0) { + if (priv->time_callback) { + tmp_tm = (*priv->time_callback) (dedit, priv->time_callback_data); + } else { + the_time = time (NULL); + tmp_tm = *localtime (&the_time); + } + } else { + tmp_tm = *localtime (&the_time); + } + + date_changed = e_date_edit_set_date_internal ( + dedit, TRUE, + FALSE, + tmp_tm.tm_year, + tmp_tm.tm_mon, + tmp_tm.tm_mday); + time_changed = e_date_edit_set_time_internal ( + dedit, TRUE, + FALSE, + tmp_tm.tm_hour, + tmp_tm.tm_min); + } + + e_date_edit_update_date_entry (dedit); + e_date_edit_update_time_entry (dedit); + e_date_edit_update_time_combo_state (dedit); + + /* Emit the signals if the date and/or time has actually changed. */ + if (date_changed || time_changed) + g_signal_emit (dedit, signals[CHANGED], 0); +} + +/** + * e_date_edit_get_date: + * @dedit: an #EDateEdit widget. + * @year: returns the year set. + * @month: returns the month set (1 - 12). + * @day: returns the day set (1 - 31). + * @Returns: TRUE if a time was set, or FALSE if the field is empty or 'None'. + * + * Returns the last valid date entered into the date field. + */ +gboolean +e_date_edit_get_date (EDateEdit *dedit, + gint *year, + gint *month, + gint *day) +{ + EDateEditPrivate *priv; + + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), FALSE); + + priv = dedit->priv; + + /* Try to parse any new value now. */ + e_date_edit_check_date_changed (dedit); + + *year = priv->year + 1900; + *month = priv->month + 1; + *day = priv->day; + + if (priv->date_set_to_none + && e_date_edit_get_allow_no_date_set (dedit)) + return FALSE; + + return TRUE; +} + +/** + * e_date_edit_set_date: + * @dedit: an #EDateEdit widget. + * @year: the year to set. + * @month: the month to set (1 - 12). + * @day: the day to set (1 - 31). + * + * Sets the date in the date field. + */ +void +e_date_edit_set_date (EDateEdit *dedit, + gint year, + gint month, + gint day) +{ + gboolean date_changed = FALSE; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + date_changed = e_date_edit_set_date_internal ( + dedit, TRUE, FALSE, + year - 1900, month - 1, + day); + + e_date_edit_update_date_entry (dedit); + e_date_edit_update_time_combo_state (dedit); + + /* Emit the signals if the date has actually changed. */ + if (date_changed) + g_signal_emit (dedit, signals[CHANGED], 0); +} + +/** + * e_date_edit_get_time_of_day: + * @dedit: an #EDateEdit widget. + * @hour: returns the hour set, or 0 if the time isn't set. + * @minute: returns the minute set, or 0 if the time isn't set. + * @Returns: TRUE if a time was set, or FALSE if the field is empty or 'None'. + * + * Returns the last valid time entered into the time field. + */ +gboolean +e_date_edit_get_time_of_day (EDateEdit *dedit, + gint *hour, + gint *minute) +{ + EDateEditPrivate *priv; + + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), FALSE); + + priv = dedit->priv; + + /* Try to parse any new value now. */ + e_date_edit_check_time_changed (dedit); + + if (priv->time_set_to_none) { + *hour = 0; + *minute = 0; + return FALSE; + } else { + *hour = priv->hour; + *minute = priv->minute; + return TRUE; + } +} + +/** + * e_date_edit_set_time_of_day: + * @dedit: an #EDateEdit widget. + * @hour: the hour to set, or -1 to set the time to None (i.e. empty). + * @minute: the minute to set. + * + * Description: Sets the time in the time field. + */ +void +e_date_edit_set_time_of_day (EDateEdit *dedit, + gint hour, + gint minute) +{ + EDateEditPrivate *priv; + gboolean time_changed = FALSE; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + priv = dedit->priv; + + if (hour == -1) { + gboolean allow_no_date_set = e_date_edit_get_allow_no_date_set (dedit); + g_return_if_fail (allow_no_date_set); + if (!priv->time_set_to_none) { + priv->time_set_to_none = TRUE; + time_changed = TRUE; + } + } else if (priv->time_set_to_none + || priv->hour != hour + || priv->minute != minute) { + priv->time_set_to_none = FALSE; + priv->hour = hour; + priv->minute = minute; + time_changed = TRUE; + } + + e_date_edit_update_time_entry (dedit); + + if (time_changed) + g_signal_emit (dedit, signals[CHANGED], 0); +} + +void +e_date_edit_set_date_and_time_of_day (EDateEdit *dedit, + gint year, + gint month, + gint day, + gint hour, + gint minute) +{ + gboolean date_changed, time_changed; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + date_changed = e_date_edit_set_date_internal ( + dedit, TRUE, FALSE, + year - 1900, month - 1, day); + time_changed = e_date_edit_set_time_internal ( + dedit, TRUE, FALSE, + hour, minute); + + e_date_edit_update_date_entry (dedit); + e_date_edit_update_time_entry (dedit); + e_date_edit_update_time_combo_state (dedit); + + if (date_changed || time_changed) + g_signal_emit (dedit, signals[CHANGED], 0); +} + +/** + * e_date_edit_get_show_date: + * @dedit: an #EDateEdit widget. + * @Returns: Whether the date field is shown. + * + * Description: Returns TRUE if the date field is currently shown. + */ +gboolean +e_date_edit_get_show_date (EDateEdit *dedit) +{ + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), TRUE); + + return dedit->priv->show_date; +} + +/** + * e_date_edit_set_show_date: + * @dedit: an #EDateEdit widget. + * @show_date: TRUE if the date field should be shown. + * + * Description: Specifies whether the date field should be shown. The date + * field would be hidden if only a time needed to be entered. + */ +void +e_date_edit_set_show_date (EDateEdit *dedit, + gboolean show_date) +{ + EDateEditPrivate *priv; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + priv = dedit->priv; + + if (priv->show_date == show_date) + return; + + priv->show_date = show_date; + + if (show_date) { + gtk_widget_show (priv->date_entry); + gtk_widget_show (priv->date_button); + } else { + gtk_widget_hide (priv->date_entry); + gtk_widget_hide (priv->date_button); + } + + e_date_edit_update_time_combo_state (dedit); + + if (priv->show_date + && (priv->show_time || priv->make_time_insensitive)) + gtk_widget_show (priv->space); + else + gtk_widget_hide (priv->space); + + g_object_notify (G_OBJECT (dedit), "show-date"); +} + +/** + * e_date_edit_get_show_time: + * @dedit: an #EDateEdit widget + * @Returns: Whether the time field is shown. + * + * Description: Returns TRUE if the time field is currently shown. + */ +gboolean +e_date_edit_get_show_time (EDateEdit *dedit) +{ + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), TRUE); + + return dedit->priv->show_time; +} + +/** + * e_date_edit_set_show_time: + * @dedit: an #EDateEdit widget + * @show_time: TRUE if the time field should be shown. + * + * Description: Specifies whether the time field should be shown. The time + * field would be hidden if only a date needed to be entered. + */ +void +e_date_edit_set_show_time (EDateEdit *dedit, + gboolean show_time) +{ + EDateEditPrivate *priv; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + priv = dedit->priv; + + if (priv->show_time == show_time) + return; + + priv->show_time = show_time; + + e_date_edit_update_time_combo_state (dedit); + + g_object_notify (G_OBJECT (dedit), "show-time"); +} + +/** + * e_date_edit_get_make_time_insensitive: + * @dedit: an #EDateEdit widget + * @Returns: Whether the time field is be made insensitive instead of hiding + * it. + * + * Description: Returns TRUE if the time field is made insensitive instead of + * hiding it. + */ +gboolean +e_date_edit_get_make_time_insensitive (EDateEdit *dedit) +{ + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), TRUE); + + return dedit->priv->make_time_insensitive; +} + +/** + * e_date_edit_set_make_time_insensitive: + * @dedit: an #EDateEdit widget + * @make_insensitive: TRUE if the time field should be made insensitive instead + * of hiding it. + * + * Description: Specifies whether the time field should be made insensitive + * rather than hiding it. Note that this doesn't make it insensitive - you + * need to call e_date_edit_set_show_time() with FALSE as show_time to do that. + * + * This is useful if you want to disable the time field, but don't want it to + * disappear as that may affect the layout of the widgets. + */ +void +e_date_edit_set_make_time_insensitive (EDateEdit *dedit, + gboolean make_insensitive) +{ + EDateEditPrivate *priv; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + priv = dedit->priv; + + if (priv->make_time_insensitive == make_insensitive) + return; + + priv->make_time_insensitive = make_insensitive; + + e_date_edit_update_time_combo_state (dedit); +} + +/** + * e_date_edit_get_week_start_day: + * @dedit: an #EDateEdit widget + * @Returns: the week start day, from 0 (Monday) to 6 (Sunday). + * + * Description: Returns the week start day currently used in the calendar + * popup. + */ +gint +e_date_edit_get_week_start_day (EDateEdit *dedit) +{ + gint week_start_day; + + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), 1); + + g_object_get ( + E_CALENDAR (dedit->priv->calendar)->calitem, + "week_start_day", &week_start_day, NULL); + + return week_start_day; +} + +/** + * e_date_edit_set_week_start_day: + * @dedit: an #EDateEdit widget + * @week_start_day: the week start day, from 0 (Monday) to 6 (Sunday). + * + * Description: Sets the week start day to use in the calendar popup. + */ +void +e_date_edit_set_week_start_day (EDateEdit *dedit, + gint week_start_day) +{ + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (E_CALENDAR (dedit->priv->calendar)->calitem), + "week_start_day", week_start_day, NULL); + + g_object_notify (G_OBJECT (dedit), "week-start-day"); +} + +/* Whether we show week numbers in the date popup. */ +gboolean +e_date_edit_get_show_week_numbers (EDateEdit *dedit) +{ + gboolean show_week_numbers; + + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), FALSE); + + g_object_get ( + E_CALENDAR (dedit->priv->calendar)->calitem, + "show_week_numbers", &show_week_numbers, NULL); + + return show_week_numbers; +} + +void +e_date_edit_set_show_week_numbers (EDateEdit *dedit, + gboolean show_week_numbers) +{ + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (E_CALENDAR (dedit->priv->calendar)->calitem), + "show_week_numbers", show_week_numbers, NULL); + + g_object_notify (G_OBJECT (dedit), "show-week-numbers"); +} + +/* Whether we use 24 hour format in the time field & popup. */ +gboolean +e_date_edit_get_use_24_hour_format (EDateEdit *dedit) +{ + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), TRUE); + + return dedit->priv->use_24_hour_format; +} + +void +e_date_edit_set_use_24_hour_format (EDateEdit *dedit, + gboolean use_24_hour_format) +{ + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + if (dedit->priv->use_24_hour_format == use_24_hour_format) + return; + + dedit->priv->use_24_hour_format = use_24_hour_format; + + rebuild_time_popup (dedit); + + e_date_edit_update_time_entry (dedit); + + g_object_notify (G_OBJECT (dedit), "use-24-hour-format"); +} + +/* Whether we allow the date to be set to 'None'. e_date_edit_get_time() will + * return (time_t) -1 in this case. */ +gboolean +e_date_edit_get_allow_no_date_set (EDateEdit *dedit) +{ + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), FALSE); + + return dedit->priv->allow_no_date_set; +} + +void +e_date_edit_set_allow_no_date_set (EDateEdit *dedit, + gboolean allow_no_date_set) +{ + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + if (dedit->priv->allow_no_date_set == allow_no_date_set) + return; + + dedit->priv->allow_no_date_set = allow_no_date_set; + + if (!allow_no_date_set) { + /* If the date is showing, we make sure it isn't 'None' (we + * don't really mind if the time is empty), else if just the + * time is showing we make sure it isn't 'None'. */ + if (dedit->priv->show_date) { + if (dedit->priv->date_set_to_none) + e_date_edit_set_time (dedit, 0); + } else { + if (dedit->priv->time_set_to_none) + e_date_edit_set_time (dedit, 0); + } + } + + g_object_notify (G_OBJECT (dedit), "allow-no-date-set"); +} + +/* The range of time to show in the time combo popup. */ +void +e_date_edit_get_time_popup_range (EDateEdit *dedit, + gint *lower_hour, + gint *upper_hour) +{ + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + *lower_hour = dedit->priv->lower_hour; + *upper_hour = dedit->priv->upper_hour; +} + +void +e_date_edit_set_time_popup_range (EDateEdit *dedit, + gint lower_hour, + gint upper_hour) +{ + EDateEditPrivate *priv; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + priv = dedit->priv; + + if (priv->lower_hour == lower_hour + && priv->upper_hour == upper_hour) + return; + + priv->lower_hour = lower_hour; + priv->upper_hour = upper_hour; + + rebuild_time_popup (dedit); + + /* Setting the combo list items seems to mess up the time entry, so + * we set it again. We have to reset it to its last valid time. */ + priv->time_is_valid = TRUE; + e_date_edit_update_time_entry (dedit); +} + +/* The arrow button beside the date field has been clicked, so we show the + * popup with the ECalendar in. */ +static void +on_date_button_clicked (GtkWidget *widget, + EDateEdit *dedit) +{ + GdkEvent *event; + + /* Obtain the GdkEvent that triggered + * the date button's "clicked" signal. */ + event = gtk_get_current_event (); + e_date_edit_show_date_popup (dedit, event); +} + +static void +e_date_edit_show_date_popup (EDateEdit *dedit, + GdkEvent *event) +{ + EDateEditPrivate *priv; + ECalendar *calendar; + GdkDevice *event_device; + GdkDevice *assoc_device; + GdkDevice *keyboard_device; + GdkDevice *pointer_device; + GdkWindow *window; + GdkGrabStatus grab_status; + struct tm mtm; + const gchar *date_text; + GDate selected_day; + gboolean clear_selection = FALSE; + guint event_time; + + priv = dedit->priv; + calendar = E_CALENDAR (priv->calendar); + + date_text = gtk_entry_get_text (GTK_ENTRY (priv->date_entry)); + if (field_set_to_none (date_text) + || !e_date_edit_parse_date (dedit, date_text, &mtm)) + clear_selection = TRUE; + + if (clear_selection) { + e_calendar_item_set_selection (calendar->calitem, NULL, NULL); + } else { + g_date_clear (&selected_day, 1); + g_date_set_dmy ( + &selected_day, mtm.tm_mday, mtm.tm_mon + 1, + mtm.tm_year + 1900); + e_calendar_item_set_selection ( + calendar->calitem, + &selected_day, NULL); + } + + /* FIXME: Hack. Change ECalendarItem so it doesn't queue signal + * emissions. */ + calendar->calitem->selection_changed = FALSE; + + position_date_popup (dedit); + gtk_widget_show (priv->cal_popup); + gtk_widget_grab_focus (priv->cal_popup); + gtk_grab_add (priv->cal_popup); + + window = gtk_widget_get_window (priv->cal_popup); + + g_return_if_fail (priv->grabbed_keyboard == NULL); + g_return_if_fail (priv->grabbed_pointer == NULL); + + event_device = gdk_event_get_device (event); + assoc_device = gdk_device_get_associated_device (event_device); + + event_time = gdk_event_get_time (event); + + if (gdk_device_get_source (event_device) == GDK_SOURCE_KEYBOARD) { + keyboard_device = event_device; + pointer_device = assoc_device; + } else { + keyboard_device = assoc_device; + pointer_device = event_device; + } + + if (keyboard_device != NULL) { + grab_status = gdk_device_grab ( + keyboard_device, + window, + GDK_OWNERSHIP_WINDOW, + TRUE, + GDK_KEY_PRESS_MASK | + GDK_KEY_RELEASE_MASK, + NULL, + event_time); + if (grab_status == GDK_GRAB_SUCCESS) { + priv->grabbed_keyboard = + g_object_ref (keyboard_device); + } + } + + if (pointer_device != NULL) { + grab_status = gdk_device_grab ( + pointer_device, + window, + GDK_OWNERSHIP_WINDOW, + TRUE, + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK, + NULL, + event_time); + if (grab_status == GDK_GRAB_SUCCESS) { + priv->grabbed_pointer = + g_object_ref (pointer_device); + } else if (priv->grabbed_keyboard != NULL) { + gdk_device_ungrab ( + priv->grabbed_keyboard, + event_time); + g_object_unref (priv->grabbed_keyboard); + priv->grabbed_keyboard = NULL; + } + } + + gdk_window_focus (window, event_time); +} + +/* This positions the date popup below and to the left of the arrow button, + * just before it is shown. */ +static void +position_date_popup (EDateEdit *dedit) +{ + gint x, y; + gint win_x, win_y; + gint bwidth, bheight; + GtkWidget *toplevel; + GdkWindow *window; + GtkRequisition cal_req, button_req; + gint screen_width, screen_height; + + gtk_widget_get_preferred_size (dedit->priv->cal_popup, &cal_req, NULL); + + gtk_widget_get_preferred_size (dedit->priv->date_button, &button_req, NULL); + bwidth = button_req.width; + gtk_widget_get_preferred_size ( + gtk_widget_get_parent (dedit->priv->date_button), &button_req, NULL); + bheight = button_req.height; + + gtk_widget_translate_coordinates ( + dedit->priv->date_button, + gtk_widget_get_toplevel (dedit->priv->date_button), + bwidth - cal_req.width, bheight, &x, &y); + + toplevel = gtk_widget_get_toplevel (dedit->priv->date_button); + window = gtk_widget_get_window (toplevel); + gdk_window_get_origin (window, &win_x, &win_y); + + x += win_x; + y += win_y; + + screen_width = gdk_screen_width (); + screen_height = gdk_screen_height (); + + x = CLAMP (x, 0, MAX (0, screen_width - cal_req.width)); + y = CLAMP (y, 0, MAX (0, screen_height - cal_req.height)); + + gtk_window_move (GTK_WINDOW (dedit->priv->cal_popup), x, y); +} + +/* A date has been selected in the date popup, so we set the date field + * and hide the popup. */ +static void +on_date_popup_date_selected (ECalendarItem *calitem, + EDateEdit *dedit) +{ + GDate start_date, end_date; + + hide_date_popup (dedit); + + if (!e_calendar_item_get_selection (calitem, &start_date, &end_date)) + return; + + e_date_edit_set_date ( + dedit, g_date_get_year (&start_date), + g_date_get_month (&start_date), + g_date_get_day (&start_date)); +} + +static void +on_date_popup_now_button_clicked (GtkWidget *button, + EDateEdit *dedit) +{ + hide_date_popup (dedit); + e_date_edit_set_time (dedit, 0); +} + +static void +on_date_popup_today_button_clicked (GtkWidget *button, + EDateEdit *dedit) +{ + EDateEditPrivate *priv; + struct tm tmp_tm; + time_t t; + + priv = dedit->priv; + + hide_date_popup (dedit); + + if (priv->time_callback) { + tmp_tm = (*priv->time_callback) (dedit, priv->time_callback_data); + } else { + t = time (NULL); + tmp_tm = *localtime (&t); + } + + e_date_edit_set_date ( + dedit, tmp_tm.tm_year + 1900, + tmp_tm.tm_mon + 1, tmp_tm.tm_mday); +} + +static void +on_date_popup_none_button_clicked (GtkWidget *button, + EDateEdit *dedit) +{ + hide_date_popup (dedit); + e_date_edit_set_time (dedit, -1); +} + +/* A key has been pressed while the date popup is showing. If it is the Escape + * key we hide the popup. */ +static gint +on_date_popup_key_press (GtkWidget *widget, + GdkEventKey *event, + EDateEdit *dedit) +{ + if (event->keyval == GDK_KEY_Escape) { + g_signal_stop_emission_by_name (widget, "key_press_event"); + hide_date_popup (dedit); + return TRUE; + } + + return FALSE; +} + +/* A mouse button has been pressed while the date popup is showing. + * Any button press events used to select days etc. in the popup will have + * have been handled elsewhere, so here we just hide the popup. + * (This function is yanked from gtkcombo.c) */ +static gint +on_date_popup_button_press (GtkWidget *widget, + GdkEvent *button_event, + gpointer data) +{ + EDateEdit *dedit; + GtkWidget *child; + + dedit = data; + + child = gtk_get_event_widget (button_event); + + /* We don't ask for button press events on the grab widget, so + * if an event is reported directly to the grab widget, it must + * be on a window outside the application (and thus we remove + * the popup window). Otherwise, we check if the widget is a child + * of the grab widget, and only remove the popup window if it + * is not. + */ + if (child != widget) { + while (child) { + if (child == widget) + return FALSE; + child = gtk_widget_get_parent (child); + } + } + + hide_date_popup (dedit); + + return TRUE; +} + +/* A delete event has been received for the date popup, so we hide it and + * return TRUE so it doesn't get destroyed. */ +static gint +on_date_popup_delete_event (GtkWidget *widget, + EDateEdit *dedit) +{ + hide_date_popup (dedit); + return TRUE; +} + +/* Hides the date popup, removing any grabs. */ +static void +hide_date_popup (EDateEdit *dedit) +{ + gtk_widget_hide (dedit->priv->cal_popup); + gtk_grab_remove (dedit->priv->cal_popup); + + if (dedit->priv->grabbed_keyboard != NULL) { + gdk_device_ungrab ( + dedit->priv->grabbed_keyboard, + GDK_CURRENT_TIME); + g_object_unref (dedit->priv->grabbed_keyboard); + dedit->priv->grabbed_keyboard = NULL; + } + + if (dedit->priv->grabbed_pointer != NULL) { + gdk_device_ungrab ( + dedit->priv->grabbed_pointer, + GDK_CURRENT_TIME); + g_object_unref (dedit->priv->grabbed_pointer); + dedit->priv->grabbed_pointer = NULL; + } +} + +/* Clears the time popup and rebuilds it using the lower_hour, upper_hour + * and use_24_hour_format settings. */ +static void +rebuild_time_popup (EDateEdit *dedit) +{ + EDateEditPrivate *priv; + GtkTreeModel *model; + GtkListStore *list_store; + GtkTreeIter iter; + gchar buffer[40]; + struct tm tmp_tm; + gint hour, min; + + priv = dedit->priv; + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (priv->time_combo)); + list_store = GTK_LIST_STORE (model); + gtk_list_store_clear (list_store); + + /* Fill the struct tm with some sane values. */ + tmp_tm.tm_year = 2000; + tmp_tm.tm_mon = 0; + tmp_tm.tm_mday = 1; + tmp_tm.tm_sec = 0; + tmp_tm.tm_isdst = 0; + + for (hour = priv->lower_hour; hour <= priv->upper_hour; hour++) { + + /* We don't want to display midnight at the end, + * since that is really in the next day. */ + if (hour == 24) + break; + + /* We want to finish on upper_hour, with min == 0. */ + for (min = 0; + min == 0 || (min < 60 && hour != priv->upper_hour); + min += 30) { + tmp_tm.tm_hour = hour; + tmp_tm.tm_min = min; + + if (priv->use_24_hour_format) + /* This is a strftime() format. + * %H = hour (0-23), %M = minute. */ + e_time_format_time ( + &tmp_tm, 1, 0, + buffer, sizeof (buffer)); + else + /* This is a strftime() format. + * %I = hour (1-12), %M = minute, + * %p = am/pm string. */ + e_time_format_time ( + &tmp_tm, 0, 0, + buffer, sizeof (buffer)); + + /* For 12-hour am/pm format, we want space padding, + * not zero padding. This can be done with strftime's + * %l, but it's a potentially unportable extension. */ + if (!priv->use_24_hour_format && buffer[0] == '0') + buffer[0] = ' '; + + gtk_list_store_append (list_store, &iter); + gtk_list_store_set (list_store, &iter, 0, buffer, -1); + } + } +} + +static gboolean +e_date_edit_parse_date (EDateEdit *dedit, + const gchar *date_text, + struct tm *date_tm) +{ + gboolean twodigit_year = FALSE; + + if (e_time_parse_date_ex (date_text, date_tm, &twodigit_year) != E_TIME_PARSE_OK) + return FALSE; + + if (twodigit_year && !dedit->priv->twodigit_year_can_future) { + time_t t = time (NULL); + struct tm *today_tm = localtime (&t); + + /* It was only 2 digit year in dedit and it was interpreted as + * in the future, but we don't want it as this, so decrease by + * 100 years to last century. */ + if (date_tm->tm_year > today_tm->tm_year) + date_tm->tm_year -= 100; + } + + return TRUE; +} + +static gboolean +e_date_edit_parse_time (EDateEdit *dedit, + const gchar *time_text, + struct tm *time_tm) +{ + if (field_set_to_none (time_text)) { + time_tm->tm_hour = 0; + time_tm->tm_min = 0; + return TRUE; + } + + if (e_time_parse_time (time_text, time_tm) != E_TIME_PARSE_OK) + return FALSE; + + return TRUE; +} + +/* Returns TRUE if the string is empty or is "None" in the current locale. + * It ignores whitespace. */ +static gboolean +field_set_to_none (const gchar *text) +{ + const gchar *pos; + const gchar *none_string; + gint n; + + pos = text; + while (n = (gint)((guchar) * pos), isspace (n)) + pos++; + + /* Translators: "None" for date field of a date edit, shown when + * there is no date set. */ + none_string = C_("date", "None"); + + if (*pos == '\0' || !strncmp (pos, none_string, strlen (none_string))) + return TRUE; + return FALSE; +} + +static void +on_date_edit_time_selected (GtkComboBox *combo, + EDateEdit *dedit) +{ + GtkWidget *child; + + child = gtk_bin_get_child (GTK_BIN (combo)); + + /* We only want to emit signals when an item is selected explicitly, + * not when it is selected by the silly combo update thing. */ + if (gtk_combo_box_get_active (combo) == -1) + return; + + if (!gtk_widget_get_mapped (child)) + return; + + e_date_edit_check_time_changed (dedit); +} + +static gint +on_date_entry_key_press (GtkWidget *widget, + GdkEvent *key_event, + EDateEdit *dedit) +{ + GdkModifierType event_state = 0; + guint event_keyval = 0; + + gdk_event_get_keyval (key_event, &event_keyval); + gdk_event_get_state (key_event, &event_state); + + if (event_state & GDK_MOD1_MASK + && (event_keyval == GDK_KEY_Up || event_keyval == GDK_KEY_Down + || event_keyval == GDK_KEY_Return)) { + g_signal_stop_emission_by_name (widget, "key_press_event"); + e_date_edit_show_date_popup (dedit, key_event); + return TRUE; + } + + /* If the user hits the return key emit a "date_changed" signal if + * needed. But let the signal carry on. */ + if (event_keyval == GDK_KEY_Return) { + e_date_edit_check_date_changed (dedit); + return FALSE; + } + + return FALSE; +} + +static gint +on_time_entry_key_press (GtkWidget *widget, + GdkEvent *key_event, + EDateEdit *dedit) +{ + GtkWidget *child; + GdkModifierType event_state = 0; + guint event_keyval = 0; + + gdk_event_get_keyval (key_event, &event_keyval); + gdk_event_get_state (key_event, &event_state); + + child = gtk_bin_get_child (GTK_BIN (dedit->priv->time_combo)); + + /* I'd like to use Alt+Up/Down for popping up the list, like Win32, + * but the combo steals any Up/Down keys, so we use Alt + Return. */ +#if 0 + if (event_state & GDK_MOD1_MASK + && (event_keyval == GDK_KEY_Up || event_keyval == GDK_KEY_Down)) { +#else + if (event_state & GDK_MOD1_MASK && event_keyval == GDK_KEY_Return) { +#endif + g_signal_stop_emission_by_name (widget, "key_press_event"); + g_signal_emit_by_name (child, "activate", 0); + return TRUE; + } + + /* Stop the return key from emitting the activate signal, and check + * if we need to emit a "time_changed" signal. */ + if (event_keyval == GDK_KEY_Return) { + g_signal_stop_emission_by_name (widget, "key_press_event"); + e_date_edit_check_time_changed (dedit); + return TRUE; + } + + return FALSE; +} + +static gint +on_date_entry_key_release (GtkWidget *widget, + GdkEvent *key_event, + EDateEdit *dedit) +{ + e_date_edit_check_date_changed (dedit); + return TRUE; +} + +static gint +on_time_entry_key_release (GtkWidget *widget, + GdkEvent *key_event, + EDateEdit *dedit) +{ + guint event_keyval = 0; + + gdk_event_get_keyval (key_event, &event_keyval); + + if (event_keyval == GDK_KEY_Up || event_keyval == GDK_KEY_Down) { + g_signal_stop_emission_by_name (widget, "key_release_event"); + e_date_edit_check_time_changed (dedit); + return TRUE; + } + + return FALSE; +} + +static gint +on_date_entry_focus_out (GtkEntry *entry, + GdkEventFocus *event, + EDateEdit *dedit) +{ + struct tm tmp_tm; + GtkWidget *msg_dialog; + + tmp_tm.tm_year = 0; + tmp_tm.tm_mon = 0; + tmp_tm.tm_mday = 0; + + e_date_edit_check_date_changed (dedit); + + if (!e_date_edit_date_is_valid (dedit)) { + msg_dialog = gtk_message_dialog_new ( + NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_OK, + "%s", _("Invalid Date Value")); + gtk_dialog_run (GTK_DIALOG (msg_dialog)); + gtk_widget_destroy (msg_dialog); + e_date_edit_get_date ( + dedit, &tmp_tm.tm_year, + &tmp_tm.tm_mon, &tmp_tm.tm_mday); + e_date_edit_set_date ( + dedit, tmp_tm.tm_year, + tmp_tm.tm_mon, tmp_tm.tm_mday); + gtk_widget_grab_focus (GTK_WIDGET (entry)); + return FALSE; + } else if (e_date_edit_get_date ( + dedit, &tmp_tm.tm_year, &tmp_tm.tm_mon, &tmp_tm.tm_mday)) { + + e_date_edit_set_date ( + dedit,tmp_tm.tm_year,tmp_tm.tm_mon,tmp_tm.tm_mday); + + if (dedit->priv->has_been_changed) { + /* The previous one didn't emit changed signal, + * but we want it even here, thus doing itself. */ + g_signal_emit (dedit, signals[CHANGED], 0); + dedit->priv->has_been_changed = FALSE; + } + } else { + dedit->priv->date_set_to_none = TRUE; + e_date_edit_update_date_entry (dedit); + } + return FALSE; +} + +static gint +on_time_entry_focus_out (GtkEntry *entry, + GdkEventFocus *event, + EDateEdit *dedit) +{ + GtkWidget *msg_dialog; + + e_date_edit_check_time_changed (dedit); + + if (!e_date_edit_time_is_valid (dedit)) { + msg_dialog = gtk_message_dialog_new ( + NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_OK, + "%s", _("Invalid Time Value")); + gtk_dialog_run (GTK_DIALOG (msg_dialog)); + gtk_widget_destroy (msg_dialog); + e_date_edit_set_time (dedit,e_date_edit_get_time (dedit)); + gtk_widget_grab_focus (GTK_WIDGET (entry)); + return FALSE; + } + return FALSE; +} + +static void +add_relation (EDateEdit *dedit, + GtkWidget *widget) +{ + AtkObject *a11yEdit, *a11yWidget; + AtkRelationSet *set; + AtkRelation *relation; + GPtrArray *target; + gpointer target_object; + + /* add a labelled_by relation for widget for accessibility */ + + a11yEdit = gtk_widget_get_accessible (GTK_WIDGET (dedit)); + a11yWidget = gtk_widget_get_accessible (widget); + + set = atk_object_ref_relation_set (a11yWidget); + if (set != NULL) { + relation = atk_relation_set_get_relation_by_type ( + set, ATK_RELATION_LABELLED_BY); + /* check whether has a labelled_by relation already */ + if (relation != NULL) + return; + } + + set = atk_object_ref_relation_set (a11yEdit); + if (!set) + return; + + relation = atk_relation_set_get_relation_by_type ( + set, ATK_RELATION_LABELLED_BY); + if (relation != NULL) { + target = atk_relation_get_target (relation); + target_object = g_ptr_array_index (target, 0); + if (ATK_IS_OBJECT (target_object)) { + atk_object_add_relationship ( + a11yWidget, + ATK_RELATION_LABELLED_BY, + ATK_OBJECT (target_object)); + } + } +} + +/* This sets the text in the date entry according to the current settings. */ +static void +e_date_edit_update_date_entry (EDateEdit *dedit) +{ + EDateEditPrivate *priv; + gchar buffer[100]; + struct tm tmp_tm = { 0 }; + + priv = dedit->priv; + + if (priv->date_set_to_none || !priv->date_is_valid) { + gtk_entry_set_text (GTK_ENTRY (priv->date_entry), C_("date", "None")); + } else { + /* This is a strftime() format for a short date. + * %x the preferred date representation for the current locale + * without the time, but is forced to use 4 digit year. */ + gchar *format = e_time_get_d_fmt_with_4digit_year (); + time_t tt; + + tmp_tm.tm_year = priv->year; + tmp_tm.tm_mon = priv->month; + tmp_tm.tm_mday = priv->day; + tmp_tm.tm_isdst = -1; + + /* initialize all 'struct tm' members properly */ + tt = mktime (&tmp_tm); + if (tt && localtime (&tt)) + tmp_tm = *localtime (&tt); + + e_utf8_strftime (buffer, sizeof (buffer), format, &tmp_tm); + g_free (format); + gtk_entry_set_text (GTK_ENTRY (priv->date_entry), buffer); + } + + add_relation (dedit, priv->date_entry); + add_relation (dedit, priv->date_button); +} + +/* This sets the text in the time entry according to the current settings. */ +static void +e_date_edit_update_time_entry (EDateEdit *dedit) +{ + EDateEditPrivate *priv; + GtkComboBox *combo_box; + GtkWidget *child; + gchar buffer[40]; + struct tm tmp_tm = { 0 }; + + priv = dedit->priv; + + combo_box = GTK_COMBO_BOX (priv->time_combo); + child = gtk_bin_get_child (GTK_BIN (priv->time_combo)); + + if (priv->time_set_to_none || !priv->time_is_valid) { + gtk_combo_box_set_active (combo_box, -1); + gtk_entry_set_text (GTK_ENTRY (child), ""); + } else { + GtkTreeModel *model; + GtkTreeIter iter; + gboolean valid; + gchar *b; + + /* Set these to reasonable values just in case. */ + tmp_tm.tm_year = 2000; + tmp_tm.tm_mon = 0; + tmp_tm.tm_mday = 1; + + tmp_tm.tm_hour = priv->hour; + tmp_tm.tm_min = priv->minute; + + tmp_tm.tm_sec = 0; + tmp_tm.tm_isdst = -1; + + if (priv->use_24_hour_format) + /* This is a strftime() format. + * %H = hour (0-23), %M = minute. */ + e_time_format_time ( + &tmp_tm, 1, 0, buffer, sizeof (buffer)); + else + /* This is a strftime() format. + * %I = hour (1-12), %M = minute, %p = am/pm. */ + e_time_format_time ( + &tmp_tm, 0, 0, buffer, sizeof (buffer)); + + /* For 12-hour am/pm format, we want space padding, not + * zero padding. This can be done with strftime's %l, + * but it's a potentially unportable extension. */ + if (!priv->use_24_hour_format && buffer[0] == '0') + buffer[0] = ' '; + + gtk_entry_set_text (GTK_ENTRY (child), buffer); + + /* truncate left spaces */ + b = buffer; + while (*b == ' ') + b++; + + model = gtk_combo_box_get_model (combo_box); + valid = gtk_tree_model_get_iter_first (model, &iter); + + while (valid) { + gchar *text = NULL; + + gtk_tree_model_get (model, &iter, 0, &text, -1); + if (text) { + gchar *t = text; + + /* truncate left spaces */ + while (*t == ' ') + t++; + + if (strcmp (b, t) == 0) { + gtk_combo_box_set_active_iter ( + combo_box, &iter); + g_free (text); + break; + } + } + + g_free (text); + + valid = gtk_tree_model_iter_next (model, &iter); + } + } + + add_relation (dedit, priv->time_combo); +} + +static void +e_date_edit_update_time_combo_state (EDateEdit *dedit) +{ + EDateEditPrivate *priv; + gboolean show = TRUE, show_now_button = TRUE; + gboolean clear_entry = FALSE, sensitive = TRUE; + const gchar *text; + + priv = dedit->priv; + + /* If the date entry is currently shown, and it is set to None, + * clear the time entry and disable the time combo. */ + if (priv->show_date && priv->date_set_to_none) { + clear_entry = TRUE; + sensitive = FALSE; + } + + if (!priv->show_time) { + if (priv->make_time_insensitive) { + clear_entry = TRUE; + sensitive = FALSE; + } else { + show = FALSE; + } + + show_now_button = FALSE; + } + + if (clear_entry) { + GtkWidget *child; + + /* Only clear it if it isn't empty already. */ + child = gtk_bin_get_child (GTK_BIN (priv->time_combo)); + text = gtk_entry_get_text (GTK_ENTRY (child)); + if (text[0]) + gtk_entry_set_text (GTK_ENTRY (child), ""); + } + + gtk_widget_set_sensitive (priv->time_combo, sensitive); + + if (show) + gtk_widget_show (priv->time_combo); + else + gtk_widget_hide (priv->time_combo); + + if (show_now_button) + gtk_widget_show (priv->now_button); + else + gtk_widget_hide (priv->now_button); + + if (priv->show_date + && (priv->show_time || priv->make_time_insensitive)) + gtk_widget_show (priv->space); + else + gtk_widget_hide (priv->space); +} + +/* Parses the date, and if it is different from the current settings it + * updates the settings and emits a "date_changed" signal. */ +static void +e_date_edit_check_date_changed (EDateEdit *dedit) +{ + EDateEditPrivate *priv; + const gchar *date_text; + struct tm tmp_tm; + gboolean none = FALSE, valid = TRUE, date_changed = FALSE; + + priv = dedit->priv; + + tmp_tm.tm_year = 0; + tmp_tm.tm_mon = 0; + tmp_tm.tm_mday = 0; + + date_text = gtk_entry_get_text (GTK_ENTRY (priv->date_entry)); + if (field_set_to_none (date_text)) { + none = TRUE; + } else if (!e_date_edit_parse_date (dedit, date_text, &tmp_tm)) { + valid = FALSE; + tmp_tm.tm_year = 0; + tmp_tm.tm_mon = 0; + tmp_tm.tm_mday = 0; + } + + date_changed = e_date_edit_set_date_internal ( + dedit, valid, none, + tmp_tm.tm_year, + tmp_tm.tm_mon, + tmp_tm.tm_mday); + + if (date_changed) { + priv->has_been_changed = TRUE; + g_signal_emit (dedit, signals[CHANGED], 0); + } +} + +/* Parses the time, and if it is different from the current settings it + * updates the settings and emits a "time_changed" signal. */ +static void +e_date_edit_check_time_changed (EDateEdit *dedit) +{ + EDateEditPrivate *priv; + GtkWidget *child; + const gchar *time_text; + struct tm tmp_tm; + gboolean none = FALSE, valid = TRUE, time_changed; + + priv = dedit->priv; + + tmp_tm.tm_hour = 0; + tmp_tm.tm_min = 0; + + child = gtk_bin_get_child (GTK_BIN (priv->time_combo)); + time_text = gtk_entry_get_text (GTK_ENTRY (child)); + if (field_set_to_none (time_text)) + none = TRUE; + else if (!e_date_edit_parse_time (dedit, time_text, &tmp_tm)) + valid = FALSE; + + time_changed = e_date_edit_set_time_internal ( + dedit, valid, none, + tmp_tm.tm_hour, + tmp_tm.tm_min); + + if (time_changed) { + e_date_edit_update_time_entry (dedit); + g_signal_emit (dedit, signals[CHANGED], 0); + } +} + +/** + * e_date_edit_date_is_valid: + * @dedit: an #EDateEdit widget. + * @Returns: TRUE if the last date entered was valid. + * + * Returns TRUE if the last date entered was valid. + * + * Note that if this returns FALSE, you can still use e_date_edit_get_time() + * or e_date_edit_get_date() to get the last time or date entered which was + * valid. + */ +gboolean +e_date_edit_date_is_valid (EDateEdit *dedit) +{ + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), FALSE); + + if (!dedit->priv->date_is_valid) + return FALSE; + + /* If the date is empty/None and that isn't permitted, return FALSE. */ + if (dedit->priv->date_set_to_none + && !e_date_edit_get_allow_no_date_set (dedit)) + return FALSE; + + return TRUE; +} + +/** + * e_date_edit_time_is_valid: + * @dedit: an #EDateEdit widget. + * @Returns: TRUE if the last time entered was valid. + * + * Returns TRUE if the last time entered was valid. + * + * Note that if this returns FALSE, you can still use e_date_edit_get_time() + * or e_date_edit_get_time_of_day() to get the last time or time of the day + * entered which was valid. + */ +gboolean +e_date_edit_time_is_valid (EDateEdit *dedit) +{ + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), FALSE); + + if (!dedit->priv->time_is_valid) + return FALSE; + + /* If the time is empty and that isn't permitted, return FALSE. + * Note that we don't mind an empty time if the date field is shown + * - in that case we just assume 0:00. */ + if (dedit->priv->time_set_to_none && !dedit->priv->show_date + && !e_date_edit_get_allow_no_date_set (dedit)) + return FALSE; + + return TRUE; +} + +static gboolean +e_date_edit_set_date_internal (EDateEdit *dedit, + gboolean valid, + gboolean none, + gint year, + gint month, + gint day) +{ + EDateEditPrivate *priv; + gboolean date_changed = FALSE; + + priv = dedit->priv; + + if (!valid) { + /* Date is invalid. */ + if (priv->date_is_valid) { + priv->date_is_valid = FALSE; + date_changed = TRUE; + } + } else if (none) { + /* Date has been set to 'None'. */ + if (!priv->date_is_valid + || !priv->date_set_to_none) { + priv->date_is_valid = TRUE; + priv->date_set_to_none = TRUE; + date_changed = TRUE; + } + } else { + /* Date has been set to a specific date. */ + if (!priv->date_is_valid + || priv->date_set_to_none + || priv->year != year + || priv->month != month + || priv->day != day) { + priv->date_is_valid = TRUE; + priv->date_set_to_none = FALSE; + priv->year = year; + priv->month = month; + priv->day = day; + date_changed = TRUE; + } + } + + return date_changed; +} + +static gboolean +e_date_edit_set_time_internal (EDateEdit *dedit, + gboolean valid, + gboolean none, + gint hour, + gint minute) +{ + EDateEditPrivate *priv; + gboolean time_changed = FALSE; + + priv = dedit->priv; + + if (!valid) { + /* Time is invalid. */ + if (priv->time_is_valid) { + priv->time_is_valid = FALSE; + time_changed = TRUE; + } + } else if (none) { + /* Time has been set to empty/'None'. */ + if (!priv->time_is_valid + || !priv->time_set_to_none) { + priv->time_is_valid = TRUE; + priv->time_set_to_none = TRUE; + time_changed = TRUE; + } + } else { + /* Time has been set to a specific time. */ + if (!priv->time_is_valid + || priv->time_set_to_none + || priv->hour != hour + || priv->minute != minute) { + priv->time_is_valid = TRUE; + priv->time_set_to_none = FALSE; + priv->hour = hour; + priv->minute = minute; + time_changed = TRUE; + } + } + + return time_changed; +} + +gboolean +e_date_edit_get_twodigit_year_can_future (EDateEdit *dedit) +{ + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), FALSE); + + return dedit->priv->twodigit_year_can_future; +} + +void +e_date_edit_set_twodigit_year_can_future (EDateEdit *dedit, + gboolean value) +{ + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + dedit->priv->twodigit_year_can_future = value; +} + +/* Sets a callback to use to get the current time. This is useful if the + * application needs to use its own timezone data rather than rely on the + * Unix timezone. */ +void +e_date_edit_set_get_time_callback (EDateEdit *dedit, + EDateEditGetTimeCallback cb, + gpointer data, + GDestroyNotify destroy) +{ + EDateEditPrivate *priv; + + g_return_if_fail (E_IS_DATE_EDIT (dedit)); + + priv = dedit->priv; + + if (priv->time_callback_data && priv->time_callback_destroy) + (*priv->time_callback_destroy) (priv->time_callback_data); + + priv->time_callback = cb; + priv->time_callback_data = data; + priv->time_callback_destroy = destroy; + +} + +GtkWidget * +e_date_edit_get_entry (EDateEdit *dedit) +{ + g_return_val_if_fail (E_IS_DATE_EDIT (dedit), NULL); + + return GTK_WIDGET (dedit->priv->date_entry); +} diff --git a/e-util/e-dateedit.h b/e-util/e-dateedit.h new file mode 100644 index 0000000000..b415847b23 --- /dev/null +++ b/e-util/e-dateedit.h @@ -0,0 +1,219 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* + * Author : + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * Based on the GnomeDateEdit, part of the Gnome Library. + * Copyright (C) 1997, 1998, 1999, 2000 Free Software Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +/* + * EDateEdit - a widget based on GnomeDateEdit to provide a date & optional + * time field with popups for entering a date. + * + * It emits a "changed" signal when the date and/or time has changed. + * You can check if the last date or time entered was invalid by + * calling e_date_edit_date_is_valid() and e_date_edit_time_is_valid(). + * + * Note that when the user types in a date or time, it will only emit the + * signals when the user presses the return key or switches the keyboard + * focus to another widget, or you call one of the _get_time/date functions. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_DATE_EDIT_H +#define E_DATE_EDIT_H + +#include <time.h> +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_DATE_EDIT \ + (e_date_edit_get_type ()) +#define E_DATE_EDIT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_DATE_EDIT, EDateEdit)) +#define E_DATE_EDIT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_DATE_EDIT, EDateEditClass)) +#define E_IS_DATE_EDIT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_DATE_EDIT)) +#define E_IS_DATE_EDIT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_DATE_EDIT)) +#define E_DATE_EDIT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_DATE_EDIT, EDateEditClass)) + +G_BEGIN_DECLS + +typedef struct _EDateEdit EDateEdit; +typedef struct _EDateEditClass EDateEditClass; +typedef struct _EDateEditPrivate EDateEditPrivate; + +/* The type of the callback function optionally used to get the current time. + */ +typedef struct tm (*EDateEditGetTimeCallback) + (EDateEdit *dedit, + gpointer data); + +struct _EDateEdit { + GtkBox hbox; + EDateEditPrivate *priv; +}; + +struct _EDateEditClass { + GtkBoxClass parent_class; + + /* Signals */ + void (*changed) (EDateEdit *dedit); +}; + +GType e_date_edit_get_type (void); +GtkWidget * e_date_edit_new (void); + +/* Analogous to gtk_editable_set_editable. disable editing, while still + * allowing selection. */ +void e_date_edit_set_editable (EDateEdit *dedit, + gboolean editable); + +/* Returns TRUE if the last date and time set were valid. The date and time + * are only set when the user hits Return or switches keyboard focus, or + * selects a date or time from the popup. */ +gboolean e_date_edit_date_is_valid (EDateEdit *dedit); +gboolean e_date_edit_time_is_valid (EDateEdit *dedit); + +/* Returns the last valid date & time set, or -1 if the date & time was set to + * 'None' and this is permitted via e_date_edit_set_allow_no_date_set. */ +time_t e_date_edit_get_time (EDateEdit *dedit); +void e_date_edit_set_time (EDateEdit *dedit, + time_t the_time); + +/* This returns the last valid date set, without the time. It returns TRUE + * if a date is set, or FALSE if the date is set to 'None' and this is + * permitted via e_date_edit_set_allow_no_date_set. (Month is 1 - 12). */ +gboolean e_date_edit_get_date (EDateEdit *dedit, + gint *year, + gint *month, + gint *day); +void e_date_edit_set_date (EDateEdit *dedit, + gint year, + gint month, + gint day); + +/* This returns the last valid time set, without the date. It returns TRUE + * if a time is set, or FALSE if the time is set to 'None' and this is + * permitted via e_date_edit_set_allow_no_date_set. */ +gboolean e_date_edit_get_time_of_day (EDateEdit *dedit, + gint *hour, + gint *minute); +/* Set the time. Pass -1 as hour to set to empty. */ +void e_date_edit_set_time_of_day (EDateEdit *dedit, + gint hour, + gint minute); + +void e_date_edit_set_date_and_time_of_day + (EDateEdit *dedit, + gint year, + gint month, + gint day, + gint hour, + gint minute); + +/* Whether we show the date field. */ +gboolean e_date_edit_get_show_date (EDateEdit *dedit); +void e_date_edit_set_show_date (EDateEdit *dedit, + gboolean show_date); + +/* Whether we show the time field. */ +gboolean e_date_edit_get_show_time (EDateEdit *dedit); +void e_date_edit_set_show_time (EDateEdit *dedit, + gboolean show_time); + +/* The week start day, used in the date popup. 0 (Mon) to 6 (Sun). */ +gint e_date_edit_get_week_start_day (EDateEdit *dedit); +void e_date_edit_set_week_start_day (EDateEdit *dedit, + gint week_start_day); + +/* Whether we show week numbers in the date popup. */ +gboolean e_date_edit_get_show_week_numbers + (EDateEdit *dedit); +void e_date_edit_set_show_week_numbers + (EDateEdit *dedit, + gboolean show_week_numbers); + +/* Whether we use 24 hour format in the time field & popup. */ +gboolean e_date_edit_get_use_24_hour_format + (EDateEdit *dedit); +void e_date_edit_set_use_24_hour_format + (EDateEdit *dedit, + gboolean use_24_hour_format); + +/* Whether we allow the date to be set to 'None'. e_date_edit_get_time() will + * return (time_t) -1 in this case. */ +gboolean e_date_edit_get_allow_no_date_set + (EDateEdit *dedit); +void e_date_edit_set_allow_no_date_set + (EDateEdit *dedit, + gboolean allow_no_date_set); + +/* The range of time to show in the time combo popup. */ +void e_date_edit_get_time_popup_range + (EDateEdit *dedit, + gint *lower_hour, + gint *upper_hour); +void e_date_edit_set_time_popup_range + (EDateEdit *dedit, + gint lower_hour, + gint upper_hour); + +/* Whether the time field is made insensitive rather than hiding it. */ +gboolean e_date_edit_get_make_time_insensitive + (EDateEdit *dedit); +void e_date_edit_set_make_time_insensitive + (EDateEdit *dedit, + gboolean make_insensitive); + +/* Whether two-digit years in date could be modified as in future; default is TRUE */ +gboolean e_date_edit_get_twodigit_year_can_future + (EDateEdit *dedit); +void e_date_edit_set_twodigit_year_can_future + (EDateEdit *dedit, + gboolean value); + +/* Sets a callback to use to get the current time. This is useful if the + * application needs to use its own timezone data rather than rely on the + * Unix timezone. */ +void e_date_edit_set_get_time_callback + (EDateEdit *dedit, + EDateEditGetTimeCallback cb, + gpointer data, + GDestroyNotify destroy); + +GtkWidget * e_date_edit_get_entry (EDateEdit *dedit); + +G_END_DECLS + +#endif /* E_DATE_EDIT_H */ diff --git a/e-util/e-datetime-format.c b/e-util/e-datetime-format.c index fcd93ebfc6..d0066fbc70 100644 --- a/e-util/e-datetime-format.c +++ b/e-util/e-datetime-format.c @@ -26,7 +26,10 @@ #include <gtk/gtk.h> #include "e-datetime-format.h" -#include "e-util.h" + +#include <libedataserver/libedataserver.h> + +#include "e-misc-utils.h" #define KEYS_FILENAME "datetime-formats.ini" #define KEYS_GROUPNAME "formats" diff --git a/e-util/e-datetime-format.h b/e-util/e-datetime-format.h index 28eed151b3..5974349e06 100644 --- a/e-util/e-datetime-format.h +++ b/e-util/e-datetime-format.h @@ -20,6 +20,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef __E_DATETIME_FORMAT__ #define __E_DATETIME_FORMAT__ diff --git a/e-util/e-destination-store.c b/e-util/e-destination-store.c new file mode 100644 index 0000000000..82801f2091 --- /dev/null +++ b/e-util/e-destination-store.c @@ -0,0 +1,751 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-destination-store.c - EDestination store with GtkTreeModel interface. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <string.h> +#include <glib/gi18n-lib.h> + +#include "e-destination-store.h" + +#define ITER_IS_VALID(destination_store, iter) \ + ((iter)->stamp == (destination_store)->priv->stamp) +#define ITER_GET(iter) \ + GPOINTER_TO_INT (iter->user_data) +#define ITER_SET(destination_store, iter, index) \ + G_STMT_START { \ + (iter)->stamp = (destination_store)->priv->stamp; \ + (iter)->user_data = GINT_TO_POINTER (index); \ + } G_STMT_END + +#define E_DESTINATION_STORE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_DESTINATION_STORE, EDestinationStorePrivate)) + +struct _EDestinationStorePrivate { + GPtrArray *destinations; + gint stamp; +}; + +static GType column_types[E_DESTINATION_STORE_NUM_COLUMNS]; + +static void e_destination_store_tree_model_init (GtkTreeModelIface *iface); + +G_DEFINE_TYPE_EXTENDED ( + EDestinationStore, e_destination_store, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, e_destination_store_tree_model_init); + column_types[E_DESTINATION_STORE_COLUMN_NAME] = G_TYPE_STRING; + column_types[E_DESTINATION_STORE_COLUMN_EMAIL] = G_TYPE_STRING; + column_types[E_DESTINATION_STORE_COLUMN_ADDRESS] = G_TYPE_STRING; +) + +static GtkTreeModelFlags e_destination_store_get_flags (GtkTreeModel *tree_model); +static gint e_destination_store_get_n_columns (GtkTreeModel *tree_model); +static GType e_destination_store_get_column_type (GtkTreeModel *tree_model, + gint index); +static gboolean e_destination_store_get_iter (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreePath *path); +static void e_destination_store_get_value (GtkTreeModel *tree_model, + GtkTreeIter *iter, + gint column, + GValue *value); +static gboolean e_destination_store_iter_next (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static gboolean e_destination_store_iter_children (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent); +static gboolean e_destination_store_iter_has_child (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static gint e_destination_store_iter_n_children (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static gboolean e_destination_store_iter_nth_child (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent, + gint n); +static gboolean e_destination_store_iter_parent (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *child); + +static void destination_changed (EDestinationStore *destination_store, EDestination *destination); +static void stop_destination (EDestinationStore *destination_store, EDestination *destination); + +static void +destination_store_dispose (GObject *object) +{ + EDestinationStorePrivate *priv; + gint ii; + + priv = E_DESTINATION_STORE_GET_PRIVATE (object); + + for (ii = 0; ii < priv->destinations->len; ii++) { + EDestination *destination; + + destination = g_ptr_array_index (priv->destinations, ii); + stop_destination (E_DESTINATION_STORE (object), destination); + g_object_unref (destination); + } + g_ptr_array_set_size (priv->destinations, 0); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_destination_store_parent_class)->dispose (object); +} + +static void +destination_store_finalize (GObject *object) +{ + EDestinationStorePrivate *priv; + + priv = E_DESTINATION_STORE_GET_PRIVATE (object); + + g_ptr_array_free (priv->destinations, TRUE); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_destination_store_parent_class)->finalize (object); +} + +static void +e_destination_store_class_init (EDestinationStoreClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EDestinationStorePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = destination_store_dispose; + object_class->finalize = destination_store_finalize; +} + +static void +e_destination_store_tree_model_init (GtkTreeModelIface *iface) +{ + iface->get_flags = e_destination_store_get_flags; + iface->get_n_columns = e_destination_store_get_n_columns; + iface->get_column_type = e_destination_store_get_column_type; + iface->get_iter = e_destination_store_get_iter; + iface->get_path = e_destination_store_get_path; + iface->get_value = e_destination_store_get_value; + iface->iter_next = e_destination_store_iter_next; + iface->iter_children = e_destination_store_iter_children; + iface->iter_has_child = e_destination_store_iter_has_child; + iface->iter_n_children = e_destination_store_iter_n_children; + iface->iter_nth_child = e_destination_store_iter_nth_child; + iface->iter_parent = e_destination_store_iter_parent; +} + +static void +e_destination_store_init (EDestinationStore *destination_store) +{ + destination_store->priv = + E_DESTINATION_STORE_GET_PRIVATE (destination_store); + + destination_store->priv->destinations = g_ptr_array_new (); + destination_store->priv->stamp = g_random_int (); +} + +/** + * e_destination_store_new: + * + * Creates a new #EDestinationStore. + * + * Returns: A new #EDestinationStore. + **/ +EDestinationStore * +e_destination_store_new (void) +{ + return g_object_new (E_TYPE_DESTINATION_STORE, NULL); +} + +/* ------------------ * + * Row update helpers * + * ------------------ */ + +static void +row_deleted (EDestinationStore *destination_store, + gint n) +{ + GtkTreePath *path; + + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, n); + gtk_tree_model_row_deleted (GTK_TREE_MODEL (destination_store), path); + gtk_tree_path_free (path); +} + +static void +row_inserted (EDestinationStore *destination_store, + gint n) +{ + GtkTreePath *path; + GtkTreeIter iter; + + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, n); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (destination_store), &iter, path)) + gtk_tree_model_row_inserted (GTK_TREE_MODEL (destination_store), path, &iter); + + gtk_tree_path_free (path); +} + +static void +row_changed (EDestinationStore *destination_store, + gint n) +{ + GtkTreePath *path; + GtkTreeIter iter; + + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, n); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (destination_store), &iter, path)) + gtk_tree_model_row_changed (GTK_TREE_MODEL (destination_store), path, &iter); + + gtk_tree_path_free (path); +} + +/* ------------------- * + * Destination helpers * + * ------------------- */ + +static gint +find_destination_by_pointer (EDestinationStore *destination_store, + EDestination *destination) +{ + GPtrArray *array; + gint i; + + array = destination_store->priv->destinations; + + for (i = 0; i < array->len; i++) { + EDestination *destination_here; + + destination_here = g_ptr_array_index (array, i); + + if (destination_here == destination) + return i; + } + + return -1; +} + +static gint +find_destination_by_email (EDestinationStore *destination_store, + EDestination *destination) +{ + GPtrArray *array; + gint i; + const gchar *e_mail = e_destination_get_email (destination); + + array = destination_store->priv->destinations; + + for (i = 0; i < array->len; i++) { + EDestination *destination_here; + const gchar *mail; + + destination_here = g_ptr_array_index (array, i); + mail = e_destination_get_email (destination_here); + + if (g_str_equal (e_mail, mail)) + return i; + } + + return -1; +} + +static void +start_destination (EDestinationStore *destination_store, + EDestination *destination) +{ + g_signal_connect_swapped ( + destination, "changed", + G_CALLBACK (destination_changed), destination_store); +} + +static void +stop_destination (EDestinationStore *destination_store, + EDestination *destination) +{ + g_signal_handlers_disconnect_matched ( + destination, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, destination_store); +} + +/* --------------- * + * Signal handlers * + * --------------- */ + +static void +destination_changed (EDestinationStore *destination_store, + EDestination *destination) +{ + gint n; + + n = find_destination_by_pointer (destination_store, destination); + if (n < 0) { + g_warning ("EDestinationStore got change from unknown EDestination!"); + return; + } + + row_changed (destination_store, n); +} + +/* --------------------- * + * EDestinationStore API * + * --------------------- */ + +/** + * e_destination_store_get_destination: + * @destination_store: an #EDestinationStore + * @iter: a #GtkTreeIter + * + * Gets the #EDestination from @destination_store at @iter. + * + * Returns: An #EDestination. + **/ +EDestination * +e_destination_store_get_destination (EDestinationStore *destination_store, + GtkTreeIter *iter) +{ + GPtrArray *array; + gint index; + + g_return_val_if_fail (E_IS_DESTINATION_STORE (destination_store), NULL); + g_return_val_if_fail (ITER_IS_VALID (destination_store, iter), NULL); + + array = destination_store->priv->destinations; + index = ITER_GET (iter); + + return g_ptr_array_index (array, index); +} + +/** + * e_destination_store_list_destinations: + * @destination_store: an #EDestinationStore + * + * Gets a list of all the #EDestinations in @destination_store. + * + * Returns: A #GList of pointers to #EDestination. The list is owned + * by the caller, but the #EDestination elements aren't. + **/ +GList * +e_destination_store_list_destinations (EDestinationStore *destination_store) +{ + GList *destination_list = NULL; + GPtrArray *array; + gint i; + + g_return_val_if_fail (E_IS_DESTINATION_STORE (destination_store), NULL); + + array = destination_store->priv->destinations; + + for (i = 0; i < array->len; i++) { + EDestination *destination; + + destination = g_ptr_array_index (array, i); + destination_list = g_list_prepend (destination_list, destination); + } + + destination_list = g_list_reverse (destination_list); + + return destination_list; +} + +/** + * e_destination_store_insert_destination: + * @destination_store: an #EDestinationStore + * @index: the index at which to insert + * @destination: an #EDestination to insert + * + * Inserts @destination into @destination_store at the position + * indicated by @index. @destination_store will ref @destination. + **/ +void +e_destination_store_insert_destination (EDestinationStore *destination_store, + gint index, + EDestination *destination) +{ + GPtrArray *array; + + g_return_if_fail (E_IS_DESTINATION_STORE (destination_store)); + g_return_if_fail (index >= 0); + + if (find_destination_by_pointer (destination_store, destination) >= 0) { + g_warning ("Same destination added more than once to EDestinationStore!"); + return; + } + + g_object_ref (destination); + + array = destination_store->priv->destinations; + index = MIN (index, array->len); + + g_ptr_array_set_size (array, array->len + 1); + + if (array->len - 1 - index > 0) { + memmove ( + array->pdata + index + 1, + array->pdata + index, + (array->len - 1 - index) * sizeof (gpointer)); + } + + array->pdata[index] = destination; + start_destination (destination_store, destination); + row_inserted (destination_store, index); +} + +/** + * e_destination_store_append_destination: + * @destination_store: an #EDestinationStore + * @destination: an #EDestination + * + * Appends @destination to the list of destinations in @destination_store. + * @destination_store will ref @destination. + **/ +void +e_destination_store_append_destination (EDestinationStore *destination_store, + EDestination *destination) +{ + GPtrArray *array; + + g_return_if_fail (E_IS_DESTINATION_STORE (destination_store)); + + if (find_destination_by_email (destination_store, destination) >= 0 && !e_destination_is_evolution_list (destination)) { + g_warning ("Same destination added more than once to EDestinationStore!"); + return; + } + + array = destination_store->priv->destinations; + g_object_ref (destination); + + g_ptr_array_add (array, destination); + start_destination (destination_store, destination); + row_inserted (destination_store, array->len - 1); +} + +/** + * e_destination_store_remove_destination: + * @destination_store: an #EDestinationStore + * @destination: an #EDestination to remove + * + * Removes @destination from @destination_store. @destination_store will + * unref @destination. + **/ +void +e_destination_store_remove_destination (EDestinationStore *destination_store, + EDestination *destination) +{ + GPtrArray *array; + gint n; + + g_return_if_fail (E_IS_DESTINATION_STORE (destination_store)); + + n = find_destination_by_pointer (destination_store, destination); + if (n < 0) { + g_warning ("Tried to remove unknown destination from EDestinationStore!"); + return; + } + + stop_destination (destination_store, destination); + g_object_unref (destination); + + array = destination_store->priv->destinations; + g_ptr_array_remove_index (array, n); + row_deleted (destination_store, n); +} + +void +e_destination_store_remove_destination_nth (EDestinationStore *destination_store, + gint n) +{ + EDestination *destination; + GPtrArray *array; + + g_return_if_fail (n >= 0); + + array = destination_store->priv->destinations; + destination = g_ptr_array_index (array, n); + stop_destination (destination_store, destination); + g_object_unref (destination); + + g_ptr_array_remove_index (array, n); + row_deleted (destination_store, n); +} + +guint +e_destination_store_get_destination_count (EDestinationStore *destination_store) +{ + return destination_store->priv->destinations->len; +} + +/* ---------------- * + * GtkTreeModel API * + * ---------------- */ + +static GtkTreeModelFlags +e_destination_store_get_flags (GtkTreeModel *tree_model) +{ + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), 0); + + return GTK_TREE_MODEL_LIST_ONLY; +} + +static gint +e_destination_store_get_n_columns (GtkTreeModel *tree_model) +{ + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), 0); + + return E_CONTACT_FIELD_LAST; +} + +static GType +e_destination_store_get_column_type (GtkTreeModel *tree_model, + gint index) +{ + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), G_TYPE_INVALID); + g_return_val_if_fail (index >= 0 && index < E_DESTINATION_STORE_NUM_COLUMNS, G_TYPE_INVALID); + + return column_types[index]; +} + +static gboolean +e_destination_store_get_iter (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreePath *path) +{ + EDestinationStore *destination_store; + GPtrArray *array; + gint index; + + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), FALSE); + g_return_val_if_fail (gtk_tree_path_get_depth (path) > 0, FALSE); + + destination_store = E_DESTINATION_STORE (tree_model); + + index = gtk_tree_path_get_indices (path)[0]; + array = destination_store->priv->destinations; + + if (index >= array->len) + return FALSE; + + ITER_SET (destination_store, iter, index); + return TRUE; +} + +GtkTreePath * +e_destination_store_get_path (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + EDestinationStore *destination_store = E_DESTINATION_STORE (tree_model); + GtkTreePath *path; + gint index; + + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), NULL); + g_return_val_if_fail (ITER_IS_VALID (destination_store, iter), NULL); + + index = ITER_GET (iter); + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, index); + + return path; +} + +static gboolean +e_destination_store_iter_next (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + EDestinationStore *destination_store = E_DESTINATION_STORE (tree_model); + gint index; + + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), FALSE); + g_return_val_if_fail (ITER_IS_VALID (destination_store, iter), FALSE); + + index = ITER_GET (iter); + + if (index + 1 < destination_store->priv->destinations->len) { + ITER_SET (destination_store, iter, index + 1); + return TRUE; + } + + return FALSE; +} + +static gboolean +e_destination_store_iter_children (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent) +{ + EDestinationStore *destination_store = E_DESTINATION_STORE (tree_model); + + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), FALSE); + + /* This is a list, nodes have no children. */ + if (parent) + return FALSE; + + /* But if parent == NULL we return the list itself as children of the root. */ + if (destination_store->priv->destinations->len <= 0) + return FALSE; + + ITER_SET (destination_store, iter, 0); + return TRUE; +} + +static gboolean +e_destination_store_iter_has_child (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), FALSE); + + if (iter == NULL) + return TRUE; + + return FALSE; +} + +static gint +e_destination_store_iter_n_children (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + EDestinationStore *destination_store = E_DESTINATION_STORE (tree_model); + + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), -1); + + if (iter == NULL) + return destination_store->priv->destinations->len; + + g_return_val_if_fail (ITER_IS_VALID (destination_store, iter), -1); + return 0; +} + +static gboolean +e_destination_store_iter_nth_child (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent, + gint n) +{ + EDestinationStore *destination_store = E_DESTINATION_STORE (tree_model); + + g_return_val_if_fail (E_IS_DESTINATION_STORE (tree_model), FALSE); + + if (parent) + return FALSE; + + if (n < destination_store->priv->destinations->len) { + ITER_SET (destination_store, iter, n); + return TRUE; + } + + return FALSE; +} + +static gboolean +e_destination_store_iter_parent (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *child) +{ + return FALSE; +} + +static void +e_destination_store_get_value (GtkTreeModel *tree_model, + GtkTreeIter *iter, + gint column, + GValue *value) +{ + EDestinationStore *destination_store = E_DESTINATION_STORE (tree_model); + EDestination *destination; + GString *string_new; + EContact *contact; + GPtrArray *array; + const gchar *string; + gint row; + + g_return_if_fail (E_IS_DESTINATION_STORE (tree_model)); + g_return_if_fail (column < E_DESTINATION_STORE_NUM_COLUMNS); + g_return_if_fail (ITER_IS_VALID (destination_store, iter)); + + g_value_init (value, column_types[column]); + + array = destination_store->priv->destinations; + + row = ITER_GET (iter); + if (row >= array->len) + return; + + destination = g_ptr_array_index (array, row); + g_assert (destination); + + switch (column) { + case E_DESTINATION_STORE_COLUMN_NAME: + string = e_destination_get_name (destination); + g_value_set_string (value, string); + break; + + case E_DESTINATION_STORE_COLUMN_EMAIL: + string = e_destination_get_email (destination); + g_value_set_string (value, string); + break; + + case E_DESTINATION_STORE_COLUMN_ADDRESS: + contact = e_destination_get_contact (destination); + if (contact && E_IS_CONTACT (contact)) { + if (e_contact_get (contact, E_CONTACT_IS_LIST)) { + string = e_destination_get_name (destination); + string_new = g_string_new (string); + string_new = g_string_append (string_new, " mailing list"); + g_value_set_string (value, string_new->str); + g_string_free (string_new, TRUE); + } + else { + string = e_destination_get_address (destination); + g_value_set_string (value, string); + } + } + else { + string = e_destination_get_address (destination); + g_value_set_string (value, string); + + } + break; + + default: + g_assert_not_reached (); + break; + } +} + +/** + * e_destination_store_get_stamp: + * @destination_store: an #EDestinationStore + * + * Since: 2.32 + **/ +gint +e_destination_store_get_stamp (EDestinationStore *destination_store) +{ + g_return_val_if_fail (E_IS_DESTINATION_STORE (destination_store), 0); + + return destination_store->priv->stamp; +} diff --git a/e-util/e-destination-store.h b/e-util/e-destination-store.h new file mode 100644 index 0000000000..630db11f58 --- /dev/null +++ b/e-util/e-destination-store.h @@ -0,0 +1,106 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-destination-store.h - EDestination store with GtkTreeModel interface. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_DESTINATION_STORE_H +#define E_DESTINATION_STORE_H + +#include <gtk/gtk.h> +#include <libebook/libebook.h> + +/* Standard GObject macros */ +#define E_TYPE_DESTINATION_STORE \ + (e_destination_store_get_type ()) +#define E_DESTINATION_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_DESTINATION_STORE, EDestinationStore)) +#define E_DESTINATION_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_DESTINATION_STORE, EDestinationStoreClass)) +#define E_IS_DESTINATION_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_DESTINATION_STORE)) +#define E_IS_DESTINATION_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_DESTINATION_STORE)) +#define E_DESTINATION_STORE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_DESTINATION_STORE, EDestinationStoreClass)) + +G_BEGIN_DECLS + +typedef struct _EDestinationStore EDestinationStore; +typedef struct _EDestinationStoreClass EDestinationStoreClass; +typedef struct _EDestinationStorePrivate EDestinationStorePrivate; + +struct _EDestinationStore { + GObject parent; + EDestinationStorePrivate *priv; +}; + +struct _EDestinationStoreClass { + GObjectClass parent_class; +}; + +typedef enum { + E_DESTINATION_STORE_COLUMN_NAME, + E_DESTINATION_STORE_COLUMN_EMAIL, + E_DESTINATION_STORE_COLUMN_ADDRESS, + E_DESTINATION_STORE_NUM_COLUMNS +} EDestinationStoreColumnType; + +GType e_destination_store_get_type (void); +EDestinationStore * + e_destination_store_new (void); +EDestination * e_destination_store_get_destination + (EDestinationStore *destination_store, + GtkTreeIter *iter); + +/* Returns a shallow copy; free the list when done, but don't unref elements */ +GList * e_destination_store_list_destinations + (EDestinationStore *destination_store); + +void e_destination_store_insert_destination + (EDestinationStore *destination_store, + gint index, + EDestination *destination); +void e_destination_store_append_destination + (EDestinationStore *destination_store, + EDestination *destination); +void e_destination_store_remove_destination + (EDestinationStore *destination_store, + EDestination *destination); +void e_destination_store_remove_destination_nth + (EDestinationStore *destination_store, + gint n); +guint e_destination_store_get_destination_count + (EDestinationStore *destination_store); +GtkTreePath * e_destination_store_get_path (GtkTreeModel *tree_model, + GtkTreeIter *iter); +gint e_destination_store_get_stamp (EDestinationStore *destination_store); + +G_END_DECLS + +#endif /* E_DESTINATION_STORE_H */ diff --git a/e-util/e-dialog-utils.h b/e-util/e-dialog-utils.h index f4f04b0eac..36c1730a09 100644 --- a/e-util/e-dialog-utils.h +++ b/e-util/e-dialog-utils.h @@ -20,6 +20,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_DIALOG_UTILS_H #define E_DIALOG_UTILS_H diff --git a/e-util/e-dialog-widgets.h b/e-util/e-dialog-widgets.h index 5b3f650ed2..4c8ade4426 100644 --- a/e-util/e-dialog-widgets.h +++ b/e-util/e-dialog-widgets.h @@ -22,6 +22,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_DIALOG_WIDGETS_H #define E_DIALOG_WIDGETS_H diff --git a/e-util/e-event.h b/e-util/e-event.h index 0b834c879d..28caded6fe 100644 --- a/e-util/e-event.h +++ b/e-util/e-event.h @@ -21,6 +21,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + /* This a bit 'whipped together', so is likely to change mid-term */ #ifndef E_EVENT_H @@ -197,7 +201,7 @@ void e_event_target_free (EEvent *event, /* For events, the plugin item talks to a specific instance, rather than * a set of instances of the hook handler */ -#include "e-util/e-plugin.h" +#include <e-util/e-plugin.h> /* Standard GObject macros */ #define E_TYPE_EVENT_HOOK \ diff --git a/e-util/e-file-request.c b/e-util/e-file-request.c index 724680a280..4ec56d2829 100644 --- a/e-util/e-file-request.c +++ b/e-util/e-file-request.c @@ -22,8 +22,6 @@ #include <libsoup/soup.h> -#include <e-util/e-util.h> - #include <string.h> #define d(x) diff --git a/e-util/e-file-request.h b/e-util/e-file-request.h index b8dd278c87..5d1cb3a8d6 100644 --- a/e-util/e-file-request.h +++ b/e-util/e-file-request.h @@ -16,6 +16,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_FILE_REQUEST_H #define E_FILE_REQUEST_H diff --git a/e-util/e-file-utils.h b/e-util/e-file-utils.h index e1e8b29872..5d5df061e3 100644 --- a/e-util/e-file-utils.h +++ b/e-util/e-file-utils.h @@ -20,6 +20,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_FILE_UTILS_H #define E_FILE_UTILS_H diff --git a/e-util/e-filter-code.c b/e-util/e-filter-code.c new file mode 100644 index 0000000000..0352703638 --- /dev/null +++ b/e-util/e-filter-code.c @@ -0,0 +1,102 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-filter-code.h" +#include "e-filter-part.h" + +G_DEFINE_TYPE ( + EFilterCode, + e_filter_code, + E_TYPE_FILTER_INPUT) + +/* here, the string IS the code */ +static void +filter_code_build_code (EFilterElement *element, + GString *out, + EFilterPart *part) +{ + GList *l; + EFilterInput *fi = (EFilterInput *) element; + gboolean is_rawcode = fi->type && g_str_equal (fi->type, "rawcode"); + + if (!is_rawcode) + g_string_append (out, "(match-all "); + + l = fi->values; + while (l) { + g_string_append (out, (gchar *) l->data); + l = g_list_next (l); + } + + if (!is_rawcode) + g_string_append (out, ")"); +} + +/* and we have no value */ +static void +filter_code_format_sexp (EFilterElement *element, + GString *out) +{ +} + +static void +e_filter_code_class_init (EFilterCodeClass *class) +{ + EFilterElementClass *filter_element_class; + + filter_element_class = E_FILTER_ELEMENT_CLASS (class); + filter_element_class->build_code = filter_code_build_code; + filter_element_class->format_sexp = filter_code_format_sexp; +} + +static void +e_filter_code_init (EFilterCode *code) +{ + EFilterInput *input = E_FILTER_INPUT (code); + + input->type = (gchar *) xmlStrdup ((xmlChar *) "code"); +} + +/** + * filter_code_new: + * + * Create a new EFilterCode object. + * + * Return value: A new #EFilterCode object. + **/ +EFilterCode * +e_filter_code_new (gboolean raw_code) +{ + EFilterCode *fc = g_object_new (E_TYPE_FILTER_CODE, NULL, NULL); + + if (fc && raw_code) { + xmlFree (((EFilterInput *) fc)->type); + ((EFilterInput *) fc)->type = (gchar *) xmlStrdup ((xmlChar *)"rawcode"); + } + + return fc; +} diff --git a/e-util/e-filter-code.h b/e-util/e-filter-code.h new file mode 100644 index 0000000000..45e1922ba4 --- /dev/null +++ b/e-util/e-filter-code.h @@ -0,0 +1,72 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_CODE_H +#define E_FILTER_CODE_H + +#include <e-util/e-filter-input.h> + +/* Standard GObject macros */ +#define E_TYPE_FILTER_CODE \ + (e_filter_code_get_type ()) +#define E_FILTER_CODE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_CODE, EFilterCode)) +#define E_FILTER_CODE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_CODE, EFilterCodeClass)) +#define E_IS_FILTER_CODE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_CODE)) +#define E_IS_FILTER_CODE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_CODE)) +#define E_FILTER_CODE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_CODE, EFilterCodeClass)) + +G_BEGIN_DECLS + +typedef struct _EFilterCode EFilterCode; +typedef struct _EFilterCodeClass EFilterCodeClass; +typedef struct _EFilterCodePrivate EFilterCodePrivate; + +struct _EFilterCode { + EFilterInput parent; + EFilterCodePrivate *priv; +}; + +struct _EFilterCodeClass { + EFilterInputClass parent_class; +}; + +GType e_filter_code_get_type (void); +EFilterCode * e_filter_code_new (gboolean raw_code); + +G_END_DECLS + +#endif /* E_FILTER_CODE_H */ diff --git a/e-util/e-filter-color.c b/e-util/e-filter-color.c new file mode 100644 index 0000000000..213530fbb2 --- /dev/null +++ b/e-util/e-filter-color.c @@ -0,0 +1,163 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> + +#include "e-filter-color.h" + +G_DEFINE_TYPE ( + EFilterColor, + e_filter_color, + E_TYPE_FILTER_ELEMENT) + +static void +set_color (GtkColorButton *color_button, + EFilterColor *fc) +{ + gtk_color_button_get_color (color_button, &fc->color); +} + +static gint +filter_color_eq (EFilterElement *element_a, + EFilterElement *element_b) +{ + EFilterColor *color_a = E_FILTER_COLOR (element_a); + EFilterColor *color_b = E_FILTER_COLOR (element_b); + + return E_FILTER_ELEMENT_CLASS (e_filter_color_parent_class)-> + eq (element_a, element_b) && + gdk_color_equal (&color_a->color, &color_b->color); +} + +static xmlNodePtr +filter_color_xml_encode (EFilterElement *element) +{ + EFilterColor *fc = E_FILTER_COLOR (element); + xmlNodePtr value; + gchar spec[16]; + + g_snprintf ( + spec, sizeof (spec), "#%04x%04x%04x", + fc->color.red, fc->color.green, fc->color.blue); + + value = xmlNewNode (NULL, (xmlChar *)"value"); + xmlSetProp (value, (xmlChar *)"type", (xmlChar *)"colour"); + xmlSetProp (value, (xmlChar *)"name", (xmlChar *) element->name); + xmlSetProp (value, (xmlChar *)"spec", (xmlChar *) spec); + + return value; +} + +static gint +filter_color_xml_decode (EFilterElement *element, + xmlNodePtr node) +{ + EFilterColor *fc = E_FILTER_COLOR (element); + xmlChar *prop; + + xmlFree (element->name); + element->name = (gchar *) xmlGetProp (node, (xmlChar *)"name"); + + prop = xmlGetProp (node, (xmlChar *)"spec"); + if (prop != NULL) { + gdk_color_parse ((gchar *) prop, &fc->color); + xmlFree (prop); + } else { + /* try reading the old RGB properties */ + prop = xmlGetProp (node, (xmlChar *)"red"); + sscanf ((gchar *) prop, "%" G_GINT16_MODIFIER "x", &fc->color.red); + xmlFree (prop); + prop = xmlGetProp (node, (xmlChar *)"green"); + sscanf ((gchar *) prop, "%" G_GINT16_MODIFIER "x", &fc->color.green); + xmlFree (prop); + prop = xmlGetProp (node, (xmlChar *)"blue"); + sscanf ((gchar *) prop, "%" G_GINT16_MODIFIER "x", &fc->color.blue); + xmlFree (prop); + } + + return 0; +} + +static GtkWidget * +filter_color_get_widget (EFilterElement *element) +{ + EFilterColor *fc = E_FILTER_COLOR (element); + GtkWidget *color_button; + + color_button = gtk_color_button_new_with_color (&fc->color); + gtk_widget_show (color_button); + + g_signal_connect ( + color_button, "color_set", + G_CALLBACK (set_color), element); + + return color_button; +} + +static void +filter_color_format_sexp (EFilterElement *element, + GString *out) +{ + EFilterColor *fc = E_FILTER_COLOR (element); + gchar spec[16]; + + g_snprintf ( + spec, sizeof (spec), "#%04x%04x%04x", + fc->color.red, fc->color.green, fc->color.blue); + camel_sexp_encode_string (out, spec); +} + +static void +e_filter_color_class_init (EFilterColorClass *class) +{ + EFilterElementClass *filter_element_class; + + filter_element_class = E_FILTER_ELEMENT_CLASS (class); + filter_element_class->eq = filter_color_eq; + filter_element_class->xml_encode = filter_color_xml_encode; + filter_element_class->xml_decode = filter_color_xml_decode; + filter_element_class->get_widget = filter_color_get_widget; + filter_element_class->format_sexp = filter_color_format_sexp; +} + +static void +e_filter_color_init (EFilterColor *filter) +{ +} + +/** + * filter_color_new: + * + * Create a new EFilterColor object. + * + * Return value: A new #EFilterColor object. + **/ +EFilterColor * +e_filter_color_new (void) +{ + return g_object_new (E_TYPE_FILTER_COLOR, NULL); +} diff --git a/e-util/e-filter-color.h b/e-util/e-filter-color.h new file mode 100644 index 0000000000..acecf7d08c --- /dev/null +++ b/e-util/e-filter-color.h @@ -0,0 +1,74 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_COLOR_H +#define E_FILTER_COLOR_H + +#include <e-util/e-filter-element.h> + +/* Standard GObject macros */ +#define E_TYPE_FILTER_COLOR \ + (e_filter_color_get_type ()) +#define E_FILTER_COLOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_COLOR, EFilterColor)) +#define E_FILTER_COLOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_COLOR, EFilterColorClass)) +#define E_IS_FILTER_COLOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_COLOR)) +#define E_IS_FILTER_COLOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_COLOR)) +#define E_FILTER_COLOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_COLOR, EFilterColorClass)) + +G_BEGIN_DECLS + +typedef struct _EFilterColor EFilterColor; +typedef struct _EFilterColorClass EFilterColorClass; +typedef struct _EFilterColorPrivate EFilterColorPrivate; + +struct _EFilterColor { + EFilterElement parent; + EFilterColorPrivate *priv; + + GdkColor color; +}; + +struct _EFilterColorClass { + EFilterElementClass parent_class; +}; + +GType e_filter_color_get_type (void); +EFilterColor * e_filter_color_new (void); + +G_END_DECLS + +#endif /* E_FILTER_COLOR_H */ diff --git a/e-util/e-filter-datespec.c b/e-util/e-filter-datespec.c new file mode 100644 index 0000000000..d135358e2b --- /dev/null +++ b/e-util/e-filter-datespec.c @@ -0,0 +1,513 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <stdlib.h> +#include <time.h> +#include <math.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "e-filter-datespec.h" +#include "e-filter-part.h" +#include "e-misc-utils.h" + +#ifdef G_OS_WIN32 +#ifdef localtime_r +#undef localtime_r +#endif +#define localtime_r(tp,tmp) memcpy(tmp,localtime(tp),sizeof(struct tm)) +#endif + +#define E_FILTER_DATESPEC_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_FILTER_DATESPEC, EFilterDatespecPrivate)) + +#define d(x) + +typedef struct { + guint32 seconds; + const gchar *past_singular; + const gchar *past_plural; + const gchar *future_singular; + const gchar *future_plural; + gfloat max; +} timespan; + +#if 0 + +/* Don't delete this code, since it is needed so that xgettext can extract the translations. + * Please, keep these strings in sync with the strings in the timespans array */ + + ngettext ("1 second ago", "%d seconds ago", 1); + ngettext ("1 second in the future", "%d seconds in the future", 1); + ngettext ("1 minute ago", "%d minutes ago", 1); + ngettext ("1 minute in the future", "%d minutes in the future", 1); + ngettext ("1 hour ago", "%d hours ago", 1); + ngettext ("1 hour in the future", "%d hours in the future", 1); + ngettext ("1 day ago", "%d days ago", 1); + ngettext ("1 day in the future", "%d days in the future", 1); + ngettext ("1 week ago", "%d weeks ago", 1); + ngettext ("1 week in the future", "%d weeks in the future", 1) + ngettext ("1 month ago", "%d months ago", 1); + ngettext ("1 month in the future", "%d months in the future", 1); + ngettext ("1 year ago", "%d years ago", 1); + ngettext ("1 year in the future", "%d years in the future", 1); + +#endif + +static const timespan timespans[] = { + { 1, "1 second ago", "%d seconds ago", "1 second in the future", "%d seconds in the future", 59.0 }, + { 60, "1 minute ago", "%d minutes ago", "1 minute in the future", "%d minutes in the future", 59.0 }, + { 3600, "1 hour ago", "%d hours ago", "1 hour in the future", "%d hours in the future", 23.0 }, + { 86400, "1 day ago", "%d days ago", "1 day in the future", "%d days in the future", 31.0 }, + { 604800, "1 week ago", "%d weeks ago", "1 week in the future", "%d weeks in the future", 52.0 }, + { 2419200, "1 month ago", "%d months ago", "1 month in the future", "%d months in the future", 12.0 }, + { 31557600, "1 year ago", "%d years ago", "1 year in the future", "%d years in the future", 1000.0 }, +}; + +#define DAY_INDEX 3 + +struct _EFilterDatespecPrivate { + GtkWidget *label_button; + GtkWidget *notebook_type, *combobox_type, *calendar_specify, *spin_relative, *combobox_relative, *combobox_past_future; + EFilterDatespecType type; + gint span; +}; + +G_DEFINE_TYPE ( + EFilterDatespec, + e_filter_datespec, + E_TYPE_FILTER_ELEMENT) + +static gint +get_best_span (time_t val) +{ + gint i; + + for (i = G_N_ELEMENTS (timespans) - 1; i >= 0; i--) { + if (val % timespans[i].seconds == 0) + return i; + } + + return 0; +} + +/* sets button label */ +static void +set_button (EFilterDatespec *fds) +{ + gchar buf[128]; + gchar *label = buf; + + switch (fds->type) { + case FDST_UNKNOWN: + label = _("<click here to select a date>"); + break; + case FDST_NOW: + label = _("now"); + break; + case FDST_SPECIFIED: { + struct tm tm; + + localtime_r (&fds->value, &tm); + /* strftime for date filter display, only needs to show a day date (i.e. no time) */ + strftime (buf, sizeof (buf), _("%d-%b-%Y"), &tm); + break; } + case FDST_X_AGO: + if (fds->value == 0) + label = _("now"); + else { + gint span, count; + + span = get_best_span (fds->value); + count = fds->value / timespans[span].seconds; + sprintf (buf, ngettext (timespans[span].past_singular, timespans[span].past_plural, count), count); + } + break; + case FDST_X_FUTURE: + if (fds->value == 0) + label = _("now"); + else { + gint span, count; + + span = get_best_span (fds->value); + count = fds->value / timespans[span].seconds; + sprintf (buf, ngettext (timespans[span].future_singular, timespans[span].future_plural, count), count); + } + break; + } + + gtk_label_set_text ((GtkLabel *) fds->priv->label_button, label); +} + +static void +get_values (EFilterDatespec *fds) +{ + EFilterDatespecPrivate *p = E_FILTER_DATESPEC_GET_PRIVATE (fds); + + switch (fds->priv->type) { + case FDST_SPECIFIED: { + guint year, month, day; + struct tm tm; + + gtk_calendar_get_date ((GtkCalendar *) p->calendar_specify, &year, &month, &day); + memset (&tm, 0, sizeof (tm)); + tm.tm_mday = day; + tm.tm_year = year - 1900; + tm.tm_mon = month; + fds->value = mktime (&tm); + /* what about timezone? */ + break; } + case FDST_X_FUTURE: + case FDST_X_AGO: { + gint val; + + val = gtk_spin_button_get_value_as_int ((GtkSpinButton *) p->spin_relative); + fds->value = timespans[p->span].seconds * val; + break; } + case FDST_NOW: + default: + break; + } + + fds->type = p->type; +} + +static void +set_values (EFilterDatespec *fds) +{ + gint note_type; + + EFilterDatespecPrivate *p = E_FILTER_DATESPEC_GET_PRIVATE (fds); + + p->type = fds->type == FDST_UNKNOWN ? FDST_NOW : fds->type; + + note_type = p->type==FDST_X_FUTURE ? FDST_X_AGO : p->type; /* FUTURE and AGO use the same notebook pages/etc. */ + + switch (p->type) { + case FDST_NOW: + case FDST_UNKNOWN: + /* noop */ + break; + case FDST_SPECIFIED: + { + struct tm tm; + + localtime_r (&fds->value, &tm); + gtk_calendar_select_month ((GtkCalendar *) p->calendar_specify, tm.tm_mon, tm.tm_year + 1900); + gtk_calendar_select_day ((GtkCalendar *) p->calendar_specify, tm.tm_mday); + break; + } + case FDST_X_AGO: + p->span = get_best_span (fds->value); + gtk_spin_button_set_value ((GtkSpinButton *) p->spin_relative, fds->value / timespans[p->span].seconds); + gtk_combo_box_set_active (GTK_COMBO_BOX (p->combobox_relative), p->span); + gtk_combo_box_set_active (GTK_COMBO_BOX (p->combobox_past_future), 0); + break; + case FDST_X_FUTURE: + p->span = get_best_span (fds->value); + gtk_spin_button_set_value ((GtkSpinButton *) p->spin_relative, fds->value / timespans[p->span].seconds); + gtk_combo_box_set_active (GTK_COMBO_BOX (p->combobox_relative), p->span); + gtk_combo_box_set_active (GTK_COMBO_BOX (p->combobox_past_future), 1); + break; + } + + gtk_notebook_set_current_page ((GtkNotebook *) p->notebook_type, note_type); + gtk_combo_box_set_active (GTK_COMBO_BOX (p->combobox_type), note_type); +} + +static void +set_combobox_type (GtkComboBox *combobox, + EFilterDatespec *fds) +{ + fds->priv->type = gtk_combo_box_get_active (combobox); + gtk_notebook_set_current_page ((GtkNotebook *) fds->priv->notebook_type, fds->priv->type); +} + +static void +set_combobox_relative (GtkComboBox *combobox, + EFilterDatespec *fds) +{ + fds->priv->span = gtk_combo_box_get_active (combobox); +} + +static void +set_combobox_past_future (GtkComboBox *combobox, + EFilterDatespec *fds) +{ + if (gtk_combo_box_get_active (combobox) == 0) + fds->type = fds->priv->type = FDST_X_AGO; + else + fds->type = fds->priv->type = FDST_X_FUTURE; +} + +static void +button_clicked (GtkButton *button, + EFilterDatespec *fds) +{ + EFilterDatespecPrivate *p = E_FILTER_DATESPEC_GET_PRIVATE (fds); + GtkWidget *content_area; + GtkWidget *toplevel; + GtkDialog *dialog; + GtkBuilder *builder; + + /* XXX I think we're leaking the GtkBuilder. */ + builder = gtk_builder_new (); + e_load_ui_builder_definition (builder, "filter.ui"); + + toplevel = e_builder_get_widget (builder, "filter_datespec"); + + dialog = (GtkDialog *) gtk_dialog_new (); + gtk_window_set_title ( + GTK_WINDOW (dialog), + _("Select a time to compare against")); + gtk_dialog_add_buttons ( + dialog, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, + NULL); + + p->notebook_type = e_builder_get_widget (builder, "notebook_type"); + p->combobox_type = e_builder_get_widget (builder, "combobox_type"); + p->calendar_specify = e_builder_get_widget (builder, "calendar_specify"); + p->spin_relative = e_builder_get_widget (builder, "spin_relative"); + p->combobox_relative = e_builder_get_widget (builder, "combobox_relative"); + p->combobox_past_future = e_builder_get_widget (builder, "combobox_past_future"); + + set_values (fds); + + g_signal_connect ( + p->combobox_type, "changed", + G_CALLBACK (set_combobox_type), fds); + g_signal_connect ( + p->combobox_relative, "changed", + G_CALLBACK (set_combobox_relative), fds); + g_signal_connect ( + p->combobox_past_future, "changed", + G_CALLBACK (set_combobox_past_future), fds); + + content_area = gtk_dialog_get_content_area (dialog); + gtk_box_pack_start (GTK_BOX (content_area), toplevel, TRUE, TRUE, 3); + + if (gtk_dialog_run (dialog) == GTK_RESPONSE_OK) { + get_values (fds); + set_button (fds); + } + + gtk_widget_destroy ((GtkWidget *) dialog); +} + +static gboolean +filter_datespec_validate (EFilterElement *element, + EAlert **alert) +{ + EFilterDatespec *fds = E_FILTER_DATESPEC (element); + gboolean valid; + + g_warn_if_fail (alert == NULL || *alert == NULL); + + valid = fds->type != FDST_UNKNOWN; + if (!valid) { + if (alert) + *alert = e_alert_new ("filter:no-date", NULL); + } + + return valid; +} + +static gint +filter_datespec_eq (EFilterElement *element_a, + EFilterElement *element_b) +{ + EFilterDatespec *datespec_a = E_FILTER_DATESPEC (element_a); + EFilterDatespec *datespec_b = E_FILTER_DATESPEC (element_b); + + /* Chain up to parent's eq() method. */ + if (!E_FILTER_ELEMENT_CLASS (e_filter_datespec_parent_class)-> + eq (element_a, element_b)) + return FALSE; + + return (datespec_a->type == datespec_b->type) && + (datespec_a->value == datespec_b->value); +} + +static xmlNodePtr +filter_datespec_xml_encode (EFilterElement *element) +{ + xmlNodePtr value, work; + EFilterDatespec *fds = E_FILTER_DATESPEC (element); + gchar str[32]; + + d (printf ("Encoding datespec as xml\n")); + + value = xmlNewNode (NULL, (xmlChar *)"value"); + xmlSetProp (value, (xmlChar *)"name", (xmlChar *) element->name); + xmlSetProp (value, (xmlChar *)"type", (xmlChar *)"datespec"); + + work = xmlNewChild (value, NULL, (xmlChar *)"datespec", NULL); + sprintf (str, "%d", fds->type); + xmlSetProp (work, (xmlChar *)"type", (xmlChar *) str); + sprintf (str, "%d", (gint) fds->value); + xmlSetProp (work, (xmlChar *)"value", (xmlChar *) str); + + return value; +} + +static gint +filter_datespec_xml_decode (EFilterElement *element, + xmlNodePtr node) +{ + EFilterDatespec *fds = E_FILTER_DATESPEC (element); + xmlNodePtr n; + xmlChar *val; + + d (printf ("Decoding datespec from xml %p\n", element)); + + xmlFree (element->name); + element->name = (gchar *) xmlGetProp (node, (xmlChar *)"name"); + + n = node->children; + while (n) { + if (!strcmp ((gchar *) n->name, "datespec")) { + val = xmlGetProp (n, (xmlChar *)"type"); + fds->type = atoi ((gchar *) val); + xmlFree (val); + val = xmlGetProp (n, (xmlChar *)"value"); + fds->value = atoi ((gchar *) val); + xmlFree (val); + break; + } + n = n->next; + } + + return 0; +} + +static GtkWidget * +filter_datespec_get_widget (EFilterElement *element) +{ + EFilterDatespec *fds = E_FILTER_DATESPEC (element); + GtkWidget *button; + + fds->priv->label_button = gtk_label_new (""); + gtk_misc_set_alignment (GTK_MISC (fds->priv->label_button), 0.5, 0.5); + set_button (fds); + + button = gtk_button_new (); + gtk_container_add (GTK_CONTAINER (button), fds->priv->label_button); + g_signal_connect ( + button, "clicked", + G_CALLBACK (button_clicked), fds); + + gtk_widget_show (button); + gtk_widget_show (fds->priv->label_button); + + return button; +} + +static void +filter_datespec_format_sexp (EFilterElement *element, + GString *out) +{ + EFilterDatespec *fds = E_FILTER_DATESPEC (element); + + switch (fds->type) { + case FDST_UNKNOWN: + g_warning ("user hasn't selected a datespec yet!"); + /* fall through */ + case FDST_NOW: + g_string_append (out, "(get-current-date)"); + break; + case FDST_SPECIFIED: + g_string_append_printf (out, "%d", (gint) fds->value); + break; + case FDST_X_AGO: + switch (get_best_span (fds->value)) { + case 5: /* months */ + g_string_append_printf (out, "(get-relative-months (- 0 %d))", (gint) (fds->value / timespans[5].seconds)); + break; + case 6: /* years */ + g_string_append_printf (out, "(get-relative-months (- 0 %d))", (gint) (12 * fds->value / timespans[6].seconds)); + break; + default: + g_string_append_printf (out, "(- (get-current-date) %d)", (gint) fds->value); + break; + } + break; + case FDST_X_FUTURE: + switch (get_best_span (fds->value)) { + case 5: /* months */ + g_string_append_printf (out, "(get-relative-months %d)", (gint) (fds->value / timespans[5].seconds)); + break; + case 6: /* years */ + g_string_append_printf (out, "(get-relative-months %d)", (gint) (12 * fds->value / timespans[6].seconds)); + break; + default: + g_string_append_printf (out, "(+ (get-current-date) %d)", (gint) fds->value); + break; + } + break; + } +} + +static void +e_filter_datespec_class_init (EFilterDatespecClass *class) +{ + EFilterElementClass *filter_element_class; + + g_type_class_add_private (class, sizeof (EFilterDatespecPrivate)); + + filter_element_class = E_FILTER_ELEMENT_CLASS (class); + filter_element_class->validate = filter_datespec_validate; + filter_element_class->eq = filter_datespec_eq; + filter_element_class->xml_encode = filter_datespec_xml_encode; + filter_element_class->xml_decode = filter_datespec_xml_decode; + filter_element_class->get_widget = filter_datespec_get_widget; + filter_element_class->format_sexp = filter_datespec_format_sexp; +} + +static void +e_filter_datespec_init (EFilterDatespec *datespec) +{ + datespec->priv = E_FILTER_DATESPEC_GET_PRIVATE (datespec); + datespec->type = FDST_UNKNOWN; +} + +/** + * filter_datespec_new: + * + * Create a new EFilterDatespec object. + * + * Return value: A new #EFilterDatespec object. + **/ +EFilterDatespec * +e_filter_datespec_new (void) +{ + return g_object_new (E_TYPE_FILTER_DATESPEC, NULL); +} diff --git a/e-util/e-filter-datespec.h b/e-util/e-filter-datespec.h new file mode 100644 index 0000000000..ecc15bfdc9 --- /dev/null +++ b/e-util/e-filter-datespec.h @@ -0,0 +1,91 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_DATESPEC_H +#define E_FILTER_DATESPEC_H + +#include <time.h> +#include <e-util/e-filter-element.h> + +/* Standard GObject types */ +#define E_TYPE_FILTER_DATESPEC \ + (e_filter_datespec_get_type ()) +#define E_FILTER_DATESPEC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_DATESPEC, EFilterDatespec)) +#define E_FILTER_DATESPEC_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_DATESPEC, EFilterDatespecClass)) +#define E_IS_FILTER_DATESPEC(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_DATESPEC)) +#define E_IS_FILTER_DATESPEC_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_DATESPEC)) +#define E_FILTER_DATESPEC_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_DATESPEC, EFilterDatespecClass)) + +G_BEGIN_DECLS + +typedef struct _EFilterDatespec EFilterDatespec; +typedef struct _EFilterDatespecClass EFilterDatespecClass; +typedef struct _EFilterDatespecPrivate EFilterDatespecPrivate; + +typedef enum { + FDST_UNKNOWN = -1, + FDST_NOW, + FDST_SPECIFIED, + FDST_X_AGO, + FDST_X_FUTURE +} EFilterDatespecType; + +struct _EFilterDatespec { + EFilterElement parent; + EFilterDatespecPrivate *priv; + + EFilterDatespecType type; + + /* either a timespan, an absolute time, or 0 + * depending on type -- the above mapping to + * (X_FUTURE, X_AGO, SPECIFIED, NOW) + */ + + time_t value; +}; + +struct _EFilterDatespecClass { + EFilterElementClass parent_class; +}; + +GType e_filter_datespec_get_type (void); +EFilterDatespec * + e_filter_datespec_new (void); + +G_END_DECLS + +#endif /* E_FILTER_DATESPEC_H */ diff --git a/e-util/e-filter-element.c b/e-util/e-filter-element.c new file mode 100644 index 0000000000..e00651ec03 --- /dev/null +++ b/e-util/e-filter-element.c @@ -0,0 +1,446 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <stdlib.h> + +#include "e-filter-element.h" +#include "e-filter-part.h" + +struct _element_type { + gchar *name; + + EFilterElementFunc create; + gpointer data; +}; + +G_DEFINE_TYPE ( + EFilterElement, + e_filter_element, + G_TYPE_OBJECT) + +static gboolean +filter_element_validate (EFilterElement *element, + EAlert **alert) +{ + return TRUE; +} + +static gint +filter_element_eq (EFilterElement *element_a, + EFilterElement *element_b) +{ + return (g_strcmp0 (element_a->name, element_b->name) == 0); +} + +static void +filter_element_xml_create (EFilterElement *element, + xmlNodePtr node) +{ + element->name = (gchar *) xmlGetProp (node, (xmlChar *) "name"); +} + +static EFilterElement * +filter_element_clone (EFilterElement *element) +{ + EFilterElement *clone; + xmlNodePtr node; + + clone = g_object_new (G_OBJECT_TYPE (element), NULL); + + node = e_filter_element_xml_encode (element); + e_filter_element_xml_decode (clone, node); + xmlFreeNodeList (node); + + return clone; +} + +/* This is somewhat hackish, implement all the base cases in here */ +#include "e-filter-input.h" +#include "e-filter-option.h" +#include "e-filter-code.h" +#include "e-filter-color.h" +#include "e-filter-datespec.h" +#include "e-filter-int.h" +#include "e-filter-file.h" + +static void +filter_element_copy_value (EFilterElement *dst_element, + EFilterElement *src_element) +{ + if (E_IS_FILTER_INPUT (src_element)) { + EFilterInput *src_input; + + src_input = E_FILTER_INPUT (src_element); + + if (E_IS_FILTER_INPUT (dst_element)) { + EFilterInput *dst_input; + + dst_input = E_FILTER_INPUT (dst_element); + + if (src_input->values) + e_filter_input_set_value ( + dst_input, + src_input->values->data); + + } else if (E_IS_FILTER_INT (dst_element)) { + EFilterInt *dst_int; + + dst_int = E_FILTER_INT (dst_element); + + dst_int->val = atoi (src_input->values->data); + } + + } else if (E_IS_FILTER_COLOR (src_element)) { + EFilterColor *src_color; + + src_color = E_FILTER_COLOR (src_element); + + if (E_IS_FILTER_COLOR (dst_element)) { + EFilterColor *dst_color; + + dst_color = E_FILTER_COLOR (dst_element); + + dst_color->color = src_color->color; + } + + } else if (E_IS_FILTER_DATESPEC (src_element)) { + EFilterDatespec *src_datespec; + + src_datespec = E_FILTER_DATESPEC (src_element); + + if (E_IS_FILTER_DATESPEC (dst_element)) { + EFilterDatespec *dst_datespec; + + dst_datespec = E_FILTER_DATESPEC (dst_element); + + dst_datespec->type = src_datespec->type; + dst_datespec->value = src_datespec->value; + } + + } else if (E_IS_FILTER_INT (src_element)) { + EFilterInt *src_int; + + src_int = E_FILTER_INT (src_element); + + if (E_IS_FILTER_INT (dst_element)) { + EFilterInt *dst_int; + + dst_int = E_FILTER_INT (dst_element); + + dst_int->val = src_int->val; + + } else if (E_IS_FILTER_INPUT (dst_element)) { + EFilterInput *dst_input; + gchar *values; + + dst_input = E_FILTER_INPUT (dst_element); + + values = g_strdup_printf ("%d", src_int->val); + e_filter_input_set_value (dst_input, values); + g_free (values); + } + + } else if (E_IS_FILTER_OPTION (src_element)) { + EFilterOption *src_option; + + src_option = E_FILTER_OPTION (src_element); + + if (E_IS_FILTER_OPTION (dst_element)) { + EFilterOption *dst_option; + + dst_option = E_FILTER_OPTION (dst_element); + + if (src_option->current) + e_filter_option_set_current ( + dst_option, + src_option->current->value); + } + } +} + +static void +filter_element_finalize (GObject *object) +{ + EFilterElement *element = E_FILTER_ELEMENT (object); + + xmlFree (element->name); + + /* Chain up to parent's finalize () method. */ + G_OBJECT_CLASS (e_filter_element_parent_class)->finalize (object); +} + +static void +e_filter_element_class_init (EFilterElementClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = filter_element_finalize; + + class->validate = filter_element_validate; + class->eq = filter_element_eq; + class->xml_create = filter_element_xml_create; + class->clone = filter_element_clone; + class->copy_value = filter_element_copy_value; +} + +static void +e_filter_element_init (EFilterElement *element) +{ +} + +/** + * filter_element_new: + * + * Create a new EFilterElement object. + * + * Return value: A new #EFilterElement object. + **/ +EFilterElement * +e_filter_element_new (void) +{ + return g_object_new (E_TYPE_FILTER_ELEMENT, NULL); +} + +gboolean +e_filter_element_validate (EFilterElement *element, + EAlert **alert) +{ + EFilterElementClass *class; + + g_return_val_if_fail (E_IS_FILTER_ELEMENT (element), FALSE); + + class = E_FILTER_ELEMENT_GET_CLASS (element); + g_return_val_if_fail (class->validate != NULL, FALSE); + + return class->validate (element, alert); +} + +gint +e_filter_element_eq (EFilterElement *element_a, + EFilterElement *element_b) +{ + EFilterElementClass *class; + + g_return_val_if_fail (E_IS_FILTER_ELEMENT (element_a), FALSE); + g_return_val_if_fail (E_IS_FILTER_ELEMENT (element_b), FALSE); + + /* The elements must be the same type. */ + if (G_OBJECT_TYPE (element_a) != G_OBJECT_TYPE (element_b)) + return FALSE; + + class = E_FILTER_ELEMENT_GET_CLASS (element_a); + g_return_val_if_fail (class->eq != NULL, FALSE); + + return class->eq (element_a, element_b); +} + +/** + * filter_element_xml_create: + * @fe: filter element + * @node: xml node + * + * Create a new filter element based on an xml definition of + * that element. + **/ +void +e_filter_element_xml_create (EFilterElement *element, + xmlNodePtr node) +{ + EFilterElementClass *class; + + g_return_if_fail (E_IS_FILTER_ELEMENT (element)); + g_return_if_fail (node != NULL); + + class = E_FILTER_ELEMENT_GET_CLASS (element); + g_return_if_fail (class->xml_create != NULL); + + class->xml_create (element, node); +} + +/** + * filter_element_xml_encode: + * @fe: filter element + * + * Encode the values of a filter element into xml format. + * + * Return value: + **/ +xmlNodePtr +e_filter_element_xml_encode (EFilterElement *element) +{ + EFilterElementClass *class; + + g_return_val_if_fail (E_IS_FILTER_ELEMENT (element), NULL); + + class = E_FILTER_ELEMENT_GET_CLASS (element); + g_return_val_if_fail (class->xml_encode != NULL, NULL); + + return class->xml_encode (element); +} + +/** + * filter_element_xml_decode: + * @fe: filter element + * @node: xml node + * + * Decode the values of a fitler element from xml format. + * + * Return value: + **/ +gint +e_filter_element_xml_decode (EFilterElement *element, + xmlNodePtr node) +{ + EFilterElementClass *class; + + g_return_val_if_fail (E_IS_FILTER_ELEMENT (element), FALSE); + g_return_val_if_fail (node != NULL, FALSE); + + class = E_FILTER_ELEMENT_GET_CLASS (element); + g_return_val_if_fail (class->xml_decode != NULL, FALSE); + + return class->xml_decode (element, node); +} + +/** + * filter_element_clone: + * @fe: filter element + * + * Clones the EFilterElement @fe. + * + * Return value: + **/ +EFilterElement * +e_filter_element_clone (EFilterElement *element) +{ + EFilterElementClass *class; + + g_return_val_if_fail (E_IS_FILTER_ELEMENT (element), NULL); + + class = E_FILTER_ELEMENT_GET_CLASS (element); + g_return_val_if_fail (class->clone != NULL, NULL); + + return class->clone (element); +} + +/** + * filter_element_get_widget: + * @fe: filter element + * @node: xml node + * + * Create a widget to represent this element. + * + * Return value: + **/ +GtkWidget * +e_filter_element_get_widget (EFilterElement *element) +{ + EFilterElementClass *class; + + g_return_val_if_fail (E_IS_FILTER_ELEMENT (element), NULL); + + class = E_FILTER_ELEMENT_GET_CLASS (element); + g_return_val_if_fail (class->get_widget != NULL, NULL); + + return class->get_widget (element); +} + +/** + * filter_element_build_code: + * @fe: filter element + * @out: output buffer + * @ff: + * + * Add the code representing this element to the output string @out. + **/ +void +e_filter_element_build_code (EFilterElement *element, + GString *out, + EFilterPart *part) +{ + EFilterElementClass *class; + + g_return_if_fail (E_IS_FILTER_ELEMENT (element)); + g_return_if_fail (out != NULL); + g_return_if_fail (E_IS_FILTER_PART (part)); + + class = E_FILTER_ELEMENT_GET_CLASS (element); + + /* This method is optional. */ + if (class->build_code != NULL) + class->build_code (element, out, part); +} + +/** + * filter_element_format_sexp: + * @fe: filter element + * @out: output buffer + * + * Format the value(s) of this element in a method suitable for the context of + * sexp where it is used. Usually as space separated, double-quoted strings. + **/ +void +e_filter_element_format_sexp (EFilterElement *element, + GString *out) +{ + EFilterElementClass *class; + + g_return_if_fail (E_IS_FILTER_ELEMENT (element)); + g_return_if_fail (out != NULL); + + class = E_FILTER_ELEMENT_GET_CLASS (element); + g_return_if_fail (class->format_sexp != NULL); + + class->format_sexp (element, out); +} + +void +e_filter_element_set_data (EFilterElement *element, + gpointer data) +{ + g_return_if_fail (E_IS_FILTER_ELEMENT (element)); + + element->data = data; +} + +/* only copies the value, not the name/type */ +void +e_filter_element_copy_value (EFilterElement *dst_element, + EFilterElement *src_element) +{ + EFilterElementClass *class; + + g_return_if_fail (E_IS_FILTER_ELEMENT (dst_element)); + g_return_if_fail (E_IS_FILTER_ELEMENT (src_element)); + + class = E_FILTER_ELEMENT_GET_CLASS (dst_element); + g_return_if_fail (class->copy_value != NULL); + + class->copy_value (dst_element, src_element); +} diff --git a/e-util/e-filter-element.h b/e-util/e-filter-element.h new file mode 100644 index 0000000000..ecec9db7b9 --- /dev/null +++ b/e-util/e-filter-element.h @@ -0,0 +1,125 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_ELEMENT_H +#define E_FILTER_ELEMENT_H + +#include <gtk/gtk.h> +#include <camel/camel.h> +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include <e-util/e-alert.h> + +#define E_TYPE_FILTER_ELEMENT \ + (e_filter_element_get_type ()) +#define E_FILTER_ELEMENT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_ELEMENT, EFilterElement)) +#define E_FILTER_ELEMENT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_ELEMENT, EFilterElementClass)) +#define E_IS_FILTER_ELEMENT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_ELEMENT)) +#define E_IS_FILTER_ELEMENT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_ELEMENT)) +#define E_FILTER_ELEMENT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_ELEMENT, EFilterElementClass)) + +G_BEGIN_DECLS + +struct _EFilterPart; + +typedef struct _EFilterElement EFilterElement; +typedef struct _EFilterElementClass EFilterElementClass; +typedef struct _EFilterElementPrivate EFilterElementPrivate; + +typedef EFilterElement * (*EFilterElementFunc) (gpointer data); + +struct _EFilterElement { + GObject parent; + EFilterElementPrivate *priv; + + gchar *name; + gpointer data; +}; + +struct _EFilterElementClass { + GObjectClass parent_class; + + gboolean (*validate) (EFilterElement *element, + EAlert **alert); + gint (*eq) (EFilterElement *element_a, + EFilterElement *element_b); + + void (*xml_create) (EFilterElement *element, + xmlNodePtr node); + xmlNodePtr (*xml_encode) (EFilterElement *element); + gint (*xml_decode) (EFilterElement *element, + xmlNodePtr node); + + EFilterElement *(*clone) (EFilterElement *element); + void (*copy_value) (EFilterElement *dst_element, + EFilterElement *src_element); + + GtkWidget * (*get_widget) (EFilterElement *element); + void (*build_code) (EFilterElement *element, + GString *out, + struct _EFilterPart *part); + void (*format_sexp) (EFilterElement *element, + GString *out); +}; + +GType e_filter_element_get_type (void); +EFilterElement *e_filter_element_new (void); +void e_filter_element_set_data (EFilterElement *element, + gpointer data); +gboolean e_filter_element_validate (EFilterElement *element, + EAlert **alert); +gint e_filter_element_eq (EFilterElement *element_a, + EFilterElement *element_b); +void e_filter_element_xml_create (EFilterElement *element, + xmlNodePtr node); +xmlNodePtr e_filter_element_xml_encode (EFilterElement *element); +gint e_filter_element_xml_decode (EFilterElement *element, + xmlNodePtr node); +EFilterElement *e_filter_element_clone (EFilterElement *element); +void e_filter_element_copy_value (EFilterElement *dst_element, + EFilterElement *src_element); +GtkWidget * e_filter_element_get_widget (EFilterElement *element); +void e_filter_element_build_code (EFilterElement *element, + GString *out, + struct _EFilterPart *part); +void e_filter_element_format_sexp (EFilterElement *element, + GString *out); + +G_END_DECLS + +#endif /* E_FILTER_ELEMENT_H */ diff --git a/e-util/e-filter-file.c b/e-util/e-filter-file.c new file mode 100644 index 0000000000..8e46b52a97 --- /dev/null +++ b/e-util/e-filter-file.c @@ -0,0 +1,261 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <sys/types.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> + +#include "e-alert.h" +#include "e-filter-file.h" +#include "e-filter-part.h" + +G_DEFINE_TYPE ( + EFilterFile, + e_filter_file, + E_TYPE_FILTER_ELEMENT) + +static void +filter_file_filename_changed (GtkFileChooser *file_chooser, + EFilterElement *element) +{ + EFilterFile *file = E_FILTER_FILE (element); + const gchar *path; + + path = gtk_file_chooser_get_filename (file_chooser); + + g_free (file->path); + file->path = g_strdup (path); +} + +static void +filter_file_finalize (GObject *object) +{ + EFilterFile *file = E_FILTER_FILE (object); + + xmlFree (file->type); + g_free (file->path); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_filter_file_parent_class)->finalize (object); +} + +static gboolean +filter_file_validate (EFilterElement *element, + EAlert **alert) +{ + EFilterFile *file = E_FILTER_FILE (element); + + g_warn_if_fail (alert == NULL || *alert == NULL); + + if (!file->path) { + if (alert) + *alert = e_alert_new ("filter:no-file", NULL); + return FALSE; + } + + /* FIXME: do more to validate command-lines? */ + + if (g_strcmp0 (file->type, "file") == 0) { + if (!g_file_test (file->path, G_FILE_TEST_IS_REGULAR)) { + if (alert) + *alert = e_alert_new ("filter:bad-file", + file->path, NULL); + return FALSE; + } + } else if (g_strcmp0 (file->type, "command") == 0) { + /* Only requirements so far is that the + * command can't be an empty string. */ + return (file->path[0] != '\0'); + } + + return TRUE; +} + +static gint +filter_file_eq (EFilterElement *element_a, + EFilterElement *element_b) +{ + EFilterFile *file_a = E_FILTER_FILE (element_a); + EFilterFile *file_b = E_FILTER_FILE (element_b); + + /* Chain up to parent's eq() method. */ + if (!E_FILTER_ELEMENT_CLASS (e_filter_file_parent_class)-> + eq (element_a, element_b)) + return FALSE; + + if (g_strcmp0 (file_a->path, file_b->path) != 0) + return FALSE; + + if (g_strcmp0 (file_a->type, file_b->type) != 0) + return FALSE; + + return TRUE; +} + +static xmlNodePtr +filter_file_xml_encode (EFilterElement *element) +{ + EFilterFile *file = E_FILTER_FILE (element); + xmlNodePtr cur, value; + const gchar *type; + + type = file->type ? file->type : "file"; + + value = xmlNewNode (NULL, (xmlChar *)"value"); + xmlSetProp (value, (xmlChar *) "name", (xmlChar *) element->name); + xmlSetProp (value, (xmlChar *) "type", (xmlChar *) type); + + cur = xmlNewChild (value, NULL, (xmlChar *) type, NULL); + xmlNodeSetContent (cur, (xmlChar *) file->path); + + return value; +} + +static gint +filter_file_xml_decode (EFilterElement *element, + xmlNodePtr node) +{ + EFilterFile *file = E_FILTER_FILE (element); + gchar *name, *str, *type; + xmlNodePtr child; + + name = (gchar *) xmlGetProp (node, (xmlChar *) "name"); + type = (gchar *) xmlGetProp (node, (xmlChar *) "type"); + + xmlFree (element->name); + element->name = name; + + xmlFree (file->type); + file->type = type; + + g_free (file->path); + file->path = NULL; + + child = node->children; + while (child != NULL) { + if (!strcmp ((gchar *) child->name, type)) { + str = (gchar *) xmlNodeGetContent (child); + file->path = g_strdup (str ? str : ""); + xmlFree (str); + + break; + } else if (child->type == XML_ELEMENT_NODE) { + g_warning ( + "Unknown node type '%s' encountered " + "decoding a %s\n", child->name, type); + } + + child = child->next; + } + + return 0; +} + +static GtkWidget * +filter_file_get_widget (EFilterElement *element) +{ + EFilterFile *file = E_FILTER_FILE (element); + GtkWidget *widget; + + widget = gtk_file_chooser_button_new ( + _("Choose a File"), GTK_FILE_CHOOSER_ACTION_OPEN); + gtk_file_chooser_set_filename ( + GTK_FILE_CHOOSER (widget), file->path); + g_signal_connect ( + widget, "selection-changed", + G_CALLBACK (filter_file_filename_changed), element); + + return widget; +} + +static void +filter_file_format_sexp (EFilterElement *element, + GString *out) +{ + EFilterFile *file = E_FILTER_FILE (element); + + camel_sexp_encode_string (out, file->path); +} + +static void +e_filter_file_class_init (EFilterFileClass *class) +{ + GObjectClass *object_class; + EFilterElementClass *filter_element_class; + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = filter_file_finalize; + + filter_element_class = E_FILTER_ELEMENT_CLASS (class); + filter_element_class->validate = filter_file_validate; + filter_element_class->eq = filter_file_eq; + filter_element_class->xml_encode = filter_file_xml_encode; + filter_element_class->xml_decode = filter_file_xml_decode; + filter_element_class->get_widget = filter_file_get_widget; + filter_element_class->format_sexp = filter_file_format_sexp; +} + +static void +e_filter_file_init (EFilterFile *filter) +{ +} + +/** + * filter_file_new: + * + * Create a new EFilterFile object. + * + * Return value: A new #EFilterFile object. + **/ +EFilterFile * +e_filter_file_new (void) +{ + return g_object_new (E_TYPE_FILTER_FILE, NULL); +} + +EFilterFile * +e_filter_file_new_type_name (const gchar *type) +{ + EFilterFile *file; + + file = e_filter_file_new (); + file->type = (gchar *) xmlStrdup ((xmlChar *) type); + + return file; +} + +void +e_filter_file_set_path (EFilterFile *file, + const gchar *path) +{ + g_return_if_fail (E_IS_FILTER_FILE (file)); + + g_free (file->path); + file->path = g_strdup (path); +} diff --git a/e-util/e-filter-file.h b/e-util/e-filter-file.h new file mode 100644 index 0000000000..c78062b4e0 --- /dev/null +++ b/e-util/e-filter-file.h @@ -0,0 +1,78 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_FILE_H +#define E_FILTER_FILE_H + +#include <e-util/e-filter-element.h> + +/* Standard GObject macros */ +#define E_TYPE_FILTER_FILE \ + (e_filter_file_get_type ()) +#define E_FILTER_FILE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_FILE, EFilterFile)) +#define E_FILTER_FILE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_FILE, EFilterFileClass)) +#define E_IS_FILTER_FILE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_FILE)) +#define E_IS_FILTER_FILE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_FILE)) +#define E_FILTER_FILE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_FILE, EFilterFileClass)) + +G_BEGIN_DECLS + +typedef struct _EFilterFile EFilterFile; +typedef struct _EFilterFileClass EFilterFileClass; +typedef struct _EFilterFilePrivate EFilterFilePrivate; + +struct _EFilterFile { + EFilterElement parent; + EFilterFilePrivate *priv; + + gchar *type; + gchar *path; +}; + +struct _EFilterFileClass { + EFilterElementClass parent_class; +}; + +GType e_filter_file_get_type (void); +EFilterFile * e_filter_file_new (void); +EFilterFile * e_filter_file_new_type_name (const gchar *type); +void e_filter_file_set_path (EFilterFile *file, + const gchar *path); + +G_END_DECLS + +#endif /* E_FILTER_FILE_H */ diff --git a/e-util/e-filter-input.c b/e-util/e-filter-input.c new file mode 100644 index 0000000000..424363e2dd --- /dev/null +++ b/e-util/e-filter-input.c @@ -0,0 +1,304 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <sys/types.h> +#include <regex.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "e-alert.h" +#include "e-filter-input.h" + +G_DEFINE_TYPE ( + EFilterInput, + e_filter_input, + E_TYPE_FILTER_ELEMENT) + +static void +filter_input_entry_changed (GtkEntry *entry, + EFilterElement *element) +{ + EFilterInput *input = E_FILTER_INPUT (element); + const gchar *text; + + g_list_foreach (input->values, (GFunc) g_free, NULL); + g_list_free (input->values); + + text = gtk_entry_get_text (entry); + input->values = g_list_append (NULL, g_strdup (text)); +} + +static void +filter_input_finalize (GObject *object) +{ + EFilterInput *input = E_FILTER_INPUT (object); + + xmlFree (input->type); + + g_list_foreach (input->values, (GFunc) g_free, NULL); + g_list_free (input->values); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_filter_input_parent_class)->finalize (object); +} + +static gboolean +filter_input_validate (EFilterElement *element, + EAlert **alert) +{ + EFilterInput *input = E_FILTER_INPUT (element); + gboolean valid = TRUE; + + g_warn_if_fail (alert == NULL || *alert == NULL); + + if (input->values && !strcmp (input->type, "regex")) { + const gchar *pattern; + regex_t regexpat; + gint regerr; + + pattern = input->values->data; + + regerr = regcomp ( + ®expat, pattern, + REG_EXTENDED | REG_NEWLINE | REG_ICASE); + if (regerr != 0) { + if (alert) { + gsize reglen; + gchar *regmsg; + + /* regerror gets called twice to get the full error string + * length to do proper posix error reporting */ + reglen = regerror (regerr, ®expat, 0, 0); + regmsg = g_malloc0 (reglen + 1); + regerror (regerr, ®expat, regmsg, reglen); + + *alert = e_alert_new ("filter:bad-regexp", + pattern, regmsg, NULL); + g_free (regmsg); + } + + valid = FALSE; + } + + regfree (®expat); + } + + return valid; +} + +static gint +filter_input_eq (EFilterElement *element_a, + EFilterElement *element_b) +{ + EFilterInput *input_a = E_FILTER_INPUT (element_a); + EFilterInput *input_b = E_FILTER_INPUT (element_b); + GList *link_a; + GList *link_b; + + /* Chain up to parent's eq() method. */ + if (!E_FILTER_ELEMENT_CLASS (e_filter_input_parent_class)-> + eq (element_a, element_b)) + return FALSE; + + if (g_strcmp0 (input_a->type, input_b->type) != 0) + return FALSE; + + link_a = input_a->values; + link_b = input_b->values; + + while (link_a != NULL && link_b != NULL) { + if (g_strcmp0 (link_a->data, link_b->data) != 0) + return FALSE; + + link_a = g_list_next (link_a); + link_b = g_list_next (link_b); + } + + if (link_a != NULL || link_b != NULL) + return FALSE; + + return TRUE; +} + +static xmlNodePtr +filter_input_xml_encode (EFilterElement *element) +{ + EFilterInput *input = E_FILTER_INPUT (element); + xmlNodePtr value; + GList *link; + const gchar *type; + + type = input->type ? input->type : "string"; + + value = xmlNewNode (NULL, (xmlChar *) "value"); + xmlSetProp (value, (xmlChar *) "name", (xmlChar *) element->name); + xmlSetProp (value, (xmlChar *) "type", (xmlChar *) type); + + for (link = input->values; link != NULL; link = g_list_next (link)) { + xmlChar *str = link->data; + xmlNodePtr cur; + + cur = xmlNewChild (value, NULL, (xmlChar *) type, NULL); + + str = xmlEncodeEntitiesReentrant (NULL, str); + xmlNodeSetContent (cur, str); + xmlFree (str); + } + + return value; +} + +static gint +filter_input_xml_decode (EFilterElement *element, + xmlNodePtr node) +{ + EFilterInput *input = (EFilterInput *) element; + gchar *name, *str, *type; + xmlNodePtr child; + + g_list_foreach (input->values, (GFunc) g_free, NULL); + g_list_free (input->values); + input->values = NULL; + + name = (gchar *) xmlGetProp (node, (xmlChar *) "name"); + type = (gchar *) xmlGetProp (node, (xmlChar *) "type"); + + xmlFree (element->name); + element->name = name; + + xmlFree (input->type); + input->type = type; + + child = node->children; + while (child != NULL) { + if (!strcmp ((gchar *) child->name, type)) { + if (!(str = (gchar *) xmlNodeGetContent (child))) + str = (gchar *) xmlStrdup ((xmlChar *)""); + + input->values = g_list_append (input->values, g_strdup (str)); + xmlFree (str); + } else if (child->type == XML_ELEMENT_NODE) { + g_warning ( + "Unknown node type '%s' encountered " + "decoding a %s\n", child->name, type); + } + child = child->next; + } + + return 0; +} + +static GtkWidget * +filter_input_get_widget (EFilterElement *element) +{ + EFilterInput *input = E_FILTER_INPUT (element); + GtkWidget *entry; + + entry = gtk_entry_new (); + if (input->values && input->values->data) + gtk_entry_set_text ( + GTK_ENTRY (entry), input->values->data); + + g_signal_connect ( + entry, "changed", + G_CALLBACK (filter_input_entry_changed), element); + + return entry; +} + +static void +filter_input_format_sexp (EFilterElement *element, + GString *out) +{ + EFilterInput *input = E_FILTER_INPUT (element); + GList *link; + + for (link = input->values; link != NULL; link = g_list_next (link)) + camel_sexp_encode_string (out, link->data); +} + +static void +e_filter_input_class_init (EFilterInputClass *class) +{ + GObjectClass *object_class; + EFilterElementClass *filter_element_class; + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = filter_input_finalize; + + filter_element_class = E_FILTER_ELEMENT_CLASS (class); + filter_element_class->validate = filter_input_validate; + filter_element_class->eq = filter_input_eq; + filter_element_class->xml_encode = filter_input_xml_encode; + filter_element_class->xml_decode = filter_input_xml_decode; + filter_element_class->get_widget = filter_input_get_widget; + filter_element_class->format_sexp = filter_input_format_sexp; +} + +static void +e_filter_input_init (EFilterInput *input) +{ + input->values = g_list_prepend (NULL, g_strdup ("")); +} + +/** + * filter_input_new: + * + * Create a new EFilterInput object. + * + * Return value: A new #EFilterInput object. + **/ +EFilterInput * +e_filter_input_new (void) +{ + return g_object_new (E_TYPE_FILTER_INPUT, NULL); +} + +EFilterInput * +e_filter_input_new_type_name (const gchar *type) +{ + EFilterInput *input; + + input = e_filter_input_new (); + input->type = (gchar *) xmlStrdup ((xmlChar *) type); + + return input; +} + +void +e_filter_input_set_value (EFilterInput *input, + const gchar *value) +{ + g_return_if_fail (E_IS_FILTER_INPUT (input)); + + g_list_foreach (input->values, (GFunc) g_free, NULL); + g_list_free (input->values); + + input->values = g_list_append (NULL, g_strdup (value)); +} diff --git a/e-util/e-filter-input.h b/e-util/e-filter-input.h new file mode 100644 index 0000000000..782be404e9 --- /dev/null +++ b/e-util/e-filter-input.h @@ -0,0 +1,78 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_INPUT_H +#define E_FILTER_INPUT_H + +#include <e-util/e-filter-element.h> + +/* Standard GObject macros */ +#define E_TYPE_FILTER_INPUT \ + (e_filter_input_get_type ()) +#define E_FILTER_INPUT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_INPUT, EFilterInput)) +#define E_FILTER_INPUT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_INPUT, EFilterInputClass)) +#define E_IS_FILTER_INPUT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_INPUT)) +#define E_IS_FILTER_INPUT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_INPUT)) +#define E_FILTER_INPUT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_INPUT, EFilterInputClass)) + +G_BEGIN_DECLS + +typedef struct _EFilterInput EFilterInput; +typedef struct _EFilterInputClass EFilterInputClass; +typedef struct _EFilterInputPrivate EFilterInputPrivate; + +struct _EFilterInput { + EFilterElement parent; + EFilterInputPrivate *priv; + + gchar *type; /* name of type */ + GList *values; /* strings */ +}; + +struct _EFilterInputClass { + EFilterElementClass parent_class; +}; + +GType e_filter_input_get_type (void); +EFilterInput * e_filter_input_new (void); +EFilterInput * e_filter_input_new_type_name (const gchar *type); +void e_filter_input_set_value (EFilterInput *input, + const gchar *value); + +G_END_DECLS + +#endif /* E_FILTER_INPUT_H */ diff --git a/e-util/e-filter-int.c b/e-util/e-filter-int.c new file mode 100644 index 0000000000..ba4eacde2e --- /dev/null +++ b/e-util/e-filter-int.c @@ -0,0 +1,230 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <gtk/gtk.h> + +#include "e-filter-int.h" + +G_DEFINE_TYPE ( + EFilterInt, + e_filter_int, + E_TYPE_FILTER_ELEMENT) + +static void +filter_int_spin_changed (GtkSpinButton *spin_button, + EFilterElement *element) +{ + EFilterInt *filter_int = E_FILTER_INT (element); + + filter_int->val = gtk_spin_button_get_value_as_int (spin_button); +} + +static void +filter_int_finalize (GObject *object) +{ + EFilterInt *filter_int = E_FILTER_INT (object); + + g_free (filter_int->type); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_filter_int_parent_class)->finalize (object); +} + +static gint +filter_int_eq (EFilterElement *element_a, + EFilterElement *element_b) +{ + EFilterInt *filter_int_a = E_FILTER_INT (element_a); + EFilterInt *filter_int_b = E_FILTER_INT (element_b); + + /* Chain up to parent's eq() method. */ + if (!E_FILTER_ELEMENT_CLASS (e_filter_int_parent_class)-> + eq (element_a, element_b)) + return FALSE; + + return (filter_int_a->val == filter_int_b->val); +} + +static EFilterElement * +filter_int_clone (EFilterElement *element) +{ + EFilterInt *filter_int = E_FILTER_INT (element); + EFilterInt *clone; + + clone = (EFilterInt *) e_filter_int_new_type ( + filter_int->type, filter_int->min, filter_int->max); + clone->val = filter_int->val; + + E_FILTER_ELEMENT (clone)->name = g_strdup (element->name); + + return E_FILTER_ELEMENT (clone); +} + +static xmlNodePtr +filter_int_xml_encode (EFilterElement *element) +{ + EFilterInt *filter_int = E_FILTER_INT (element); + xmlNodePtr value; + gchar intval[32]; + const gchar *type; + + type = filter_int->type ? filter_int->type : "integer"; + + value = xmlNewNode (NULL, (xmlChar *)"value"); + xmlSetProp (value, (xmlChar *) "name", (xmlChar *) element->name); + xmlSetProp (value, (xmlChar *) "type", (xmlChar *) type); + + sprintf (intval, "%d", filter_int->val); + xmlSetProp (value, (xmlChar *) type, (xmlChar *) intval); + + return value; +} + +static gint +filter_int_xml_decode (EFilterElement *element, + xmlNodePtr node) +{ + EFilterInt *filter_int = E_FILTER_INT (element); + gchar *name, *type; + gchar *intval; + + name = (gchar *) xmlGetProp (node, (xmlChar *)"name"); + xmlFree (element->name); + element->name = name; + + type = (gchar *) xmlGetProp (node, (xmlChar *)"type"); + g_free (filter_int->type); + filter_int->type = g_strdup (type); + xmlFree (type); + + intval = (gchar *) xmlGetProp ( + node, (xmlChar *) (filter_int->type ? + filter_int->type : "integer")); + if (intval) { + filter_int->val = atoi (intval); + xmlFree (intval); + } else { + filter_int->val = 0; + } + + return 0; +} + +static GtkWidget * +filter_int_get_widget (EFilterElement *element) +{ + EFilterInt *filter_int = E_FILTER_INT (element); + GtkWidget *widget; + GtkAdjustment *adjustment; + + adjustment = GTK_ADJUSTMENT (gtk_adjustment_new ( + 0.0, (gfloat) filter_int->min, + (gfloat) filter_int->max, 1.0, 1.0, 0)); + widget = gtk_spin_button_new ( + adjustment, + filter_int->max > filter_int->min + 1000 ? 5.0 : 1.0, 0); + gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (widget), TRUE); + + if (filter_int->val) + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (widget), (gfloat) filter_int->val); + + g_signal_connect ( + widget, "value-changed", + G_CALLBACK (filter_int_spin_changed), element); + + return widget; +} + +static void +filter_int_format_sexp (EFilterElement *element, + GString *out) +{ + EFilterInt *filter_int = E_FILTER_INT (element); + + if (filter_int->val < 0) + /* See #364731 #457523 C6*/ + g_string_append_printf (out, "(- 0 %d)", (filter_int->val * -1)); + else + g_string_append_printf (out, "%d", filter_int->val); +} + +static void +e_filter_int_class_init (EFilterIntClass *class) +{ + GObjectClass *object_class; + EFilterElementClass *filter_element_class; + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = filter_int_finalize; + + filter_element_class = E_FILTER_ELEMENT_CLASS (class); + filter_element_class->eq = filter_int_eq; + filter_element_class->clone = filter_int_clone; + filter_element_class->xml_encode = filter_int_xml_encode; + filter_element_class->xml_decode = filter_int_xml_decode; + filter_element_class->get_widget = filter_int_get_widget; + filter_element_class->format_sexp = filter_int_format_sexp; +} + +static void +e_filter_int_init (EFilterInt *filter_int) +{ + filter_int->min = 0; + filter_int->max = G_MAXINT; +} + +EFilterElement * +e_filter_int_new (void) +{ + return g_object_new (E_TYPE_FILTER_INT, NULL); +} + +EFilterElement * +e_filter_int_new_type (const gchar *type, + gint min, + gint max) +{ + EFilterInt *filter_int; + + filter_int = g_object_new (E_TYPE_FILTER_INT, NULL); + + filter_int->type = g_strdup (type); + filter_int->min = min; + filter_int->max = max; + + return E_FILTER_ELEMENT (filter_int); +} + +void +e_filter_int_set_value (EFilterInt *filter_int, + gint value) +{ + g_return_if_fail (E_IS_FILTER_INT (filter_int)); + + filter_int->val = value; +} diff --git a/e-util/e-filter-int.h b/e-util/e-filter-int.h new file mode 100644 index 0000000000..40b0c9e7a4 --- /dev/null +++ b/e-util/e-filter-int.h @@ -0,0 +1,81 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_INT_H +#define E_FILTER_INT_H + +#include <e-util/e-filter-element.h> + +/* Standard GObject macros */ +#define E_TYPE_FILTER_INT \ + (e_filter_int_get_type ()) +#define E_FILTER_INT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_INT, EFilterInt)) +#define E_FILTER_INT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_INT, EFilterIntClass)) +#define E_IS_FILTER_INT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_INT)) +#define E_IS_FILTER_INT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_INT)) +#define E_FILTER_INT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_INT, EFilterIntClass)) + +G_BEGIN_DECLS + +typedef struct _EFilterInt EFilterInt; +typedef struct _EFilterIntClass EFilterIntClass; +typedef struct _EFilterIntPrivate EFilterIntPrivate; + +struct _EFilterInt { + EFilterElement parent; + EFilterIntPrivate *priv; + + gchar *type; + gint val; + gint min; + gint max; +}; + +struct _EFilterIntClass { + EFilterElementClass parent_class; +}; + +GType e_filter_int_get_type (void); +EFilterElement *e_filter_int_new (void); +EFilterElement *e_filter_int_new_type (const gchar *type, + gint min, + gint max); +void e_filter_int_set_value (EFilterInt *f_int, + gint value); + +G_END_DECLS + +#endif /* E_FILTER_INT_H */ diff --git a/e-util/e-filter-option.c b/e-util/e-filter-option.c new file mode 100644 index 0000000000..630ab31916 --- /dev/null +++ b/e-util/e-filter-option.c @@ -0,0 +1,566 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gmodule.h> + +#include "e-filter-option.h" +#include "e-filter-part.h" + +G_DEFINE_TYPE ( + EFilterOption, + e_filter_option, + E_TYPE_FILTER_ELEMENT) + +static void +free_option (struct _filter_option *opt) +{ + g_free (opt->title); + g_free (opt->value); + g_free (opt->code); + g_free (opt->code_gen_func); + g_free (opt); +} + +static struct _filter_option * +find_option (EFilterOption *option, + const gchar *name) +{ + GList *link; + + for (link = option->options; link != NULL; link = g_list_next (link)) { + struct _filter_option *opt = link->data; + + if (strcmp (name, opt->value) == 0) + return opt; + } + + return NULL; +} + +static void +filter_option_combobox_changed (GtkComboBox *combo_box, + EFilterElement *element) +{ + EFilterOption *option = E_FILTER_OPTION (element); + gint active; + + active = gtk_combo_box_get_active (combo_box); + option->current = g_list_nth_data (option->options, active); +} + +static GSList * +filter_option_get_dynamic_options (EFilterOption *option) +{ + GModule *module; + GSList *(*get_func)(void); + GSList *res = NULL; + + if (!option || !option->dynamic_func) + return res; + + module = g_module_open (NULL, G_MODULE_BIND_LAZY); + + if (g_module_symbol (module, option->dynamic_func, (gpointer) &get_func)) { + res = get_func (); + } else { + g_warning ( + "optionlist dynamic fill function '%s' not found", + option->dynamic_func); + } + + g_module_close (module); + + return res; +} + +static void +filter_option_generate_code (EFilterOption *option, + GString *out, + EFilterPart *part) +{ + GModule *module; + void (*code_gen_func) (EFilterElement *element, GString *out, EFilterPart *part); + + if (!option || !option->current || !option->current->code_gen_func) + return; + + module = g_module_open (NULL, G_MODULE_BIND_LAZY); + + if (g_module_symbol (module, option->current->code_gen_func, (gpointer) &code_gen_func)) { + code_gen_func (E_FILTER_ELEMENT (option), out, part); + } else { + g_warning ( + "optionlist dynamic code function '%s' not found", + option->current->code_gen_func); + } + + g_module_close (module); +} + +static void +filter_option_finalize (GObject *object) +{ + EFilterOption *option = E_FILTER_OPTION (object); + + g_list_foreach (option->options, (GFunc) free_option, NULL); + g_list_free (option->options); + + g_free (option->dynamic_func); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_filter_option_parent_class)->finalize (object); +} + +static gint +filter_option_eq (EFilterElement *element_a, + EFilterElement *element_b) +{ + EFilterOption *option_a = E_FILTER_OPTION (element_a); + EFilterOption *option_b = E_FILTER_OPTION (element_b); + + /* Chain up to parent's eq() method. */ + if (!E_FILTER_ELEMENT_CLASS (e_filter_option_parent_class)-> + eq (element_a, element_b)) + return FALSE; + + if (option_a->current == NULL && option_b->current == NULL) + return TRUE; + + if (option_a->current == NULL || option_b->current == NULL) + return FALSE; + + return (g_strcmp0 (option_a->current->value, option_b->current->value) == 0); +} + +static void +filter_option_xml_create (EFilterElement *element, + xmlNodePtr node) +{ + EFilterOption *option = E_FILTER_OPTION (element); + xmlNodePtr n, work; + + /* Chain up to parent's xml_create() method. */ + E_FILTER_ELEMENT_CLASS (e_filter_option_parent_class)-> + xml_create (element, node); + + n = node->children; + while (n) { + if (!strcmp ((gchar *) n->name, "option")) { + gchar *tmp, *value, *title = NULL, *code = NULL, *code_gen_func = NULL; + + value = (gchar *) xmlGetProp (n, (xmlChar *)"value"); + work = n->children; + while (work) { + if (!strcmp ((gchar *) work->name, "title") || + !strcmp ((gchar *) work->name, "_title")) { + if (!title) { + if (!(tmp = (gchar *) xmlNodeGetContent (work))) + tmp = (gchar *) xmlStrdup ((xmlChar *)""); + + title = g_strdup (tmp); + xmlFree (tmp); + } + } else if (!strcmp ((gchar *) work->name, "code")) { + if (code || code_gen_func) { + g_warning ( + "Element 'code' defined twice in '%s'", + element->name); + } else { + xmlChar *fn; + + /* if element 'code' has attribute 'func', then + * the content of the element is ignored and only + * the 'func' is used to generate actual rule code; + * The function prototype is: + * void code_gen_func (EFilterElement *element, GString *out, EFilterPart *part); + * where @element is the one on which was called, + * @out is GString where to add the code, and + * @part is part which contains @element and other options of it. + */ + fn = xmlGetProp (work, (xmlChar *)"func"); + if (fn && *fn) { + code_gen_func = g_strdup ((const gchar *) fn); + } else { + if (!(tmp = (gchar *) xmlNodeGetContent (work))) + tmp = (gchar *) xmlStrdup ((xmlChar *)""); + + code = g_strdup (tmp); + xmlFree (tmp); + } + + xmlFree (fn); + } + } + work = work->next; + } + + e_filter_option_add (option, value, title, code, code_gen_func, FALSE); + xmlFree (value); + g_free (title); + g_free (code); + g_free (code_gen_func); + } else if (g_str_equal ((gchar *) n->name, "dynamic")) { + if (option->dynamic_func) { + g_warning ( + "Only one 'dynamic' node is " + "acceptable in the optionlist '%s'", + element->name); + } else { + /* Expecting only one <dynamic func="cb" /> + * in the option list, + * The 'cb' should be of this prototype: + * GSList *cb (void); + * returning GSList of struct _filter_option, + * all newly allocated, because it'll be + * freed with g_free and g_slist_free. + * 'is_dynamic' member is ignored here. + */ + xmlChar *fn; + + fn = xmlGetProp (n, (xmlChar *)"func"); + if (fn && *fn) { + GSList *items, *i; + struct _filter_option *op; + + option->dynamic_func = g_strdup ((const gchar *) fn); + + /* Get options now, to have them + * available when reading saved + * rules. */ + items = filter_option_get_dynamic_options (option); + for (i = items; i; i = i->next) { + op = i->data; + + if (op) { + e_filter_option_add ( + option, + op->value, + op->title, + op->code, + op->code_gen_func, + TRUE); + free_option (op); + } + } + + g_slist_free (items); + } else { + g_warning ( + "Missing 'func' attribute within " + "'%s' node in optionlist '%s'", + n->name, element->name); + } + + xmlFree (fn); + } + } else if (n->type == XML_ELEMENT_NODE) { + g_warning ("Unknown xml node within optionlist: %s\n", n->name); + } + n = n->next; + } +} + +static xmlNodePtr +filter_option_xml_encode (EFilterElement *element) +{ + EFilterOption *option = E_FILTER_OPTION (element); + xmlNodePtr value; + + value = xmlNewNode (NULL, (xmlChar *) "value"); + xmlSetProp (value, (xmlChar *) "name", (xmlChar *) element->name); + xmlSetProp (value, (xmlChar *) "type", (xmlChar *) option->type); + if (option->current) + xmlSetProp (value, (xmlChar *) "value", (xmlChar *) option->current->value); + + return value; +} + +static gint +filter_option_xml_decode (EFilterElement *element, + xmlNodePtr node) +{ + EFilterOption *option = E_FILTER_OPTION (element); + gchar *value; + + xmlFree (element->name); + element->name = (gchar *) xmlGetProp (node, (xmlChar *)"name"); + + value = (gchar *) xmlGetProp (node, (xmlChar *)"value"); + if (value) { + option->current = find_option (option, value); + xmlFree (value); + } else { + option->current = NULL; + } + + return 0; +} + +static EFilterElement * +filter_option_clone (EFilterElement *element) +{ + EFilterOption *option = E_FILTER_OPTION (element); + EFilterOption *clone_option; + EFilterElement *clone; + GList *link; + + /* Chain up to parent's clone() method. */ + clone = E_FILTER_ELEMENT_CLASS (e_filter_option_parent_class)-> + clone (element); + + clone_option = E_FILTER_OPTION (clone); + + for (link = option->options; link != NULL; link = g_list_next (link)) { + struct _filter_option *op = link->data; + struct _filter_option *newop; + + newop = e_filter_option_add ( + clone_option, op->value, + op->title, op->code, op->code_gen_func, op->is_dynamic); + if (option->current == op) + clone_option->current = newop; + } + + clone_option->dynamic_func = g_strdup (option->dynamic_func); + + return clone; +} + +static GtkWidget * +filter_option_get_widget (EFilterElement *element) +{ + EFilterOption *option = E_FILTER_OPTION (element); + GtkWidget *combobox; + GList *l; + struct _filter_option *op; + gint index = 0, current = 0; + + if (option->dynamic_func) { + /* it is dynamically filled, thus remove all dynamics + * and put there the fresh ones */ + GSList *items, *i; + GList *old_ops; + struct _filter_option *old_cur; + + old_ops = option->options; + old_cur = option->current; + + /* start with an empty list */ + option->current = NULL; + option->options = NULL; + + for (l = option->options; l; l = l->next) { + op = l->data; + + if (op->is_dynamic) { + break; + } else { + e_filter_option_add ( + option, op->value, op->title, + op->code, op->code_gen_func, FALSE); + } + } + + items = filter_option_get_dynamic_options (option); + for (i = items; i; i = i->next) { + op = i->data; + + if (op) { + e_filter_option_add ( + option, op->value, op->title, + op->code, op->code_gen_func, TRUE); + free_option (op); + } + } + + g_slist_free (items); + + /* maybe some static left after those dynamic, add them too */ + for (; l; l = l->next) { + op = l->data; + + if (!op->is_dynamic) + e_filter_option_add ( + option, op->value, op->title, + op->code, op->code_gen_func, FALSE); + } + + if (old_cur) + e_filter_option_set_current (option, old_cur->value); + + /* free old list */ + g_list_foreach (old_ops, (GFunc) free_option, NULL); + g_list_free (old_ops); + } + + combobox = gtk_combo_box_text_new (); + l = option->options; + while (l) { + op = l->data; + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (combobox), _(op->title)); + + if (op == option->current) + current = index; + + l = g_list_next (l); + index++; + } + + g_signal_connect ( + combobox, "changed", + G_CALLBACK (filter_option_combobox_changed), element); + + gtk_combo_box_set_active (GTK_COMBO_BOX (combobox), current); + + return combobox; +} + +static void +filter_option_build_code (EFilterElement *element, + GString *out, + EFilterPart *part) +{ + EFilterOption *option = E_FILTER_OPTION (element); + + if (option->current && option->current->code_gen_func) { + filter_option_generate_code (option, out, part); + } else if (option->current && option->current->code) { + e_filter_part_expand_code (part, option->current->code, out); + } +} + +static void +filter_option_format_sexp (EFilterElement *element, + GString *out) +{ + EFilterOption *option = E_FILTER_OPTION (element); + + if (option->current) + camel_sexp_encode_string (out, option->current->value); +} + +static void +e_filter_option_class_init (EFilterOptionClass *class) +{ + GObjectClass *object_class; + EFilterElementClass *filter_element_class; + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = filter_option_finalize; + + filter_element_class = E_FILTER_ELEMENT_CLASS (class); + filter_element_class->eq = filter_option_eq; + filter_element_class->xml_create = filter_option_xml_create; + filter_element_class->xml_encode = filter_option_xml_encode; + filter_element_class->xml_decode = filter_option_xml_decode; + filter_element_class->clone = filter_option_clone; + filter_element_class->get_widget = filter_option_get_widget; + filter_element_class->build_code = filter_option_build_code; + filter_element_class->format_sexp = filter_option_format_sexp; +} + +static void +e_filter_option_init (EFilterOption *option) +{ + option->type = "option"; + option->dynamic_func = NULL; +} + +EFilterElement * +e_filter_option_new (void) +{ + return g_object_new (E_TYPE_FILTER_OPTION, NULL); +} + +void +e_filter_option_set_current (EFilterOption *option, + const gchar *name) +{ + g_return_if_fail (E_IS_FILTER_OPTION (option)); + + option->current = find_option (option, name); +} + +/* used by implementers to add additional options */ +struct _filter_option * +e_filter_option_add (EFilterOption *option, + const gchar *value, + const gchar *title, + const gchar *code, + const gchar *code_gen_func, + gboolean is_dynamic) +{ + struct _filter_option *op; + + g_return_val_if_fail (E_IS_FILTER_OPTION (option), NULL); + g_return_val_if_fail (find_option (option, value) == NULL, NULL); + + if (code_gen_func && !*code_gen_func) + code_gen_func = NULL; + + op = g_malloc (sizeof (*op)); + op->title = g_strdup (title); + op->value = g_strdup (value); + op->code = g_strdup (code); + op->code_gen_func = g_strdup (code_gen_func); + op->is_dynamic = is_dynamic; + + option->options = g_list_append (option->options, op); + + if (option->current == NULL) + option->current = op; + + return op; +} + +const gchar * +e_filter_option_get_current (EFilterOption *option) +{ + g_return_val_if_fail (E_IS_FILTER_OPTION (option), NULL); + + if (option->current == NULL) + return NULL; + + return option->current->value; +} + +void +e_filter_option_remove_all (EFilterOption *option) +{ + g_return_if_fail (E_IS_FILTER_OPTION (option)); + + g_list_foreach (option->options, (GFunc) free_option, NULL); + g_list_free (option->options); + + option->options = NULL; + option->current = NULL; +} diff --git a/e-util/e-filter-option.h b/e-util/e-filter-option.h new file mode 100644 index 0000000000..9bd7543ba6 --- /dev/null +++ b/e-util/e-filter-option.h @@ -0,0 +1,101 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_OPTION_H +#define E_FILTER_OPTION_H + +#include <e-util/e-filter-element.h> + +/* Standard GObject macros */ +#define E_TYPE_FILTER_OPTION \ + (e_filter_option_get_type ()) +#define E_FILTER_OPTION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_OPTION, EFilterOption)) +#define E_FILTER_OPTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_OPTION, EFilterOptionClass)) +#define E_IS_FILTER_OPTION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_OPTION)) +#define E_IS_FILTER_OPTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_OPTION)) +#define E_FILTER_OPTION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_OPTION, EFilterOptionClass)) + +G_BEGIN_DECLS + +typedef struct _EFilterOption EFilterOption; +typedef struct _EFilterOptionClass EFilterOptionClass; +typedef struct _EFilterOptionPrivate EFilterOptionPrivate; + +struct _filter_option { + gchar *title; /* button title */ + gchar *value; /* value, if it has one */ + gchar *code; /* used to string code segments together */ + gchar *code_gen_func; /* function to generate the code; + * either @code or @code_gen_func is non-NULL, + * never both */ + + gboolean is_dynamic; /* whether is the option dynamic, FALSE if static; + * dynamic means "generated by EFilterOption::dynamic_func" */ +}; + +struct _EFilterOption { + EFilterElement parent; + EFilterOptionPrivate *priv; + + const gchar *type; /* static memory, type name written to xml */ + + GList *options; + struct _filter_option *current; + gchar *dynamic_func; /* name of the dynamic fill func, called in get_widget */ +}; + +struct _EFilterOptionClass { + EFilterElementClass parent_class; +}; + +GType e_filter_option_get_type (void); +EFilterElement *e_filter_option_new (void); +void e_filter_option_set_current (EFilterOption *option, + const gchar *name); +const gchar * e_filter_option_get_current (EFilterOption *option); +struct _filter_option * + e_filter_option_add (EFilterOption *option, + const gchar *name, + const gchar *title, + const gchar *code, + const gchar *code_gen_func, + gboolean is_dynamic); +void e_filter_option_remove_all (EFilterOption *option); + +G_END_DECLS + +#endif /* E_FILTER_OPTION_H */ diff --git a/e-util/e-filter-part.c b/e-util/e-filter-part.c new file mode 100644 index 0000000000..c9e14e30c6 --- /dev/null +++ b/e-util/e-filter-part.c @@ -0,0 +1,513 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jepartrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "e-filter-file.h" +#include "e-filter-part.h" +#include "e-rule-context.h" + +G_DEFINE_TYPE ( + EFilterPart, + e_filter_part, + G_TYPE_OBJECT) + +static void +filter_part_finalize (GObject *object) +{ + EFilterPart *part = E_FILTER_PART (object); + + g_list_foreach (part->elements, (GFunc) g_object_unref, NULL); + g_list_free (part->elements); + + g_free (part->name); + g_free (part->title); + g_free (part->code); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_filter_part_parent_class)->finalize (object); +} + +static void +e_filter_part_class_init (EFilterPartClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = filter_part_finalize; +} + +static void +e_filter_part_init (EFilterPart *part) +{ +} + +/** + * e_filter_part_new: + * + * Create a new EFilterPart object. + * + * Return value: A new #EFilterPart object. + **/ +EFilterPart * +e_filter_part_new (void) +{ + return g_object_new (E_TYPE_FILTER_PART, NULL); +} + +gboolean +e_filter_part_validate (EFilterPart *part, + EAlert **alert) +{ + GList *link; + + g_return_val_if_fail (E_IS_FILTER_PART (part), FALSE); + + /* The part is valid if all of its elements are valid. */ + for (link = part->elements; link != NULL; link = g_list_next (link)) { + EFilterElement *element = link->data; + + if (!e_filter_element_validate (element, alert)) + return FALSE; + } + + return TRUE; +} + +gint +e_filter_part_eq (EFilterPart *part_a, + EFilterPart *part_b) +{ + GList *link_a, *link_b; + + g_return_val_if_fail (E_IS_FILTER_PART (part_a), FALSE); + g_return_val_if_fail (E_IS_FILTER_PART (part_b), FALSE); + + if (g_strcmp0 (part_a->name, part_b->name) != 0) + return FALSE; + + if (g_strcmp0 (part_a->title, part_b->title) != 0) + return FALSE; + + if (g_strcmp0 (part_a->code, part_b->code) != 0) + return FALSE; + + link_a = part_a->elements; + link_b = part_b->elements; + + while (link_a != NULL && link_b != NULL) { + EFilterElement *element_a = link_a->data; + EFilterElement *element_b = link_b->data; + + if (!e_filter_element_eq (element_a, element_b)) + return FALSE; + + link_a = g_list_next (link_a); + link_b = g_list_next (link_b); + } + + if (link_a != NULL || link_b != NULL) + return FALSE; + + return TRUE; +} + +gint +e_filter_part_xml_create (EFilterPart *part, + xmlNodePtr node, + ERuleContext *context) +{ + xmlNodePtr n; + gchar *type, *str; + EFilterElement *el; + + g_return_val_if_fail (E_IS_FILTER_PART (part), FALSE); + g_return_val_if_fail (node != NULL, FALSE); + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), FALSE); + + str = (gchar *) xmlGetProp (node, (xmlChar *)"name"); + part->name = g_strdup (str); + if (str) + xmlFree (str); + + n = node->children; + while (n) { + if (!strcmp ((gchar *) n->name, "input")) { + type = (gchar *) xmlGetProp (n, (xmlChar *)"type"); + if (type != NULL + && (el = e_rule_context_new_element (context, type)) != NULL) { + e_filter_element_xml_create (el, n); + xmlFree (type); + part->elements = g_list_append (part->elements, el); + } else { + g_warning ("Invalid xml format, missing/unknown input type"); + } + } else if (!strcmp ((gchar *) n->name, "title") || + !strcmp ((gchar *) n->name, "_title")) { + if (!part->title) { + str = (gchar *) xmlNodeGetContent (n); + part->title = g_strdup (str); + if (str) + xmlFree (str); + } + } else if (!strcmp ((gchar *) n->name, "code")) { + if (!part->code) { + str = (gchar *) xmlNodeGetContent (n); + part->code = g_strdup (str); + if (str) + xmlFree (str); + } + } else if (n->type == XML_ELEMENT_NODE) { + g_warning ("Unknown part element in xml: %s\n", n->name); + } + n = n->next; + } + + return 0; +} + +xmlNodePtr +e_filter_part_xml_encode (EFilterPart *part) +{ + xmlNodePtr node; + GList *link; + + g_return_val_if_fail (E_IS_FILTER_PART (part), NULL); + + node = xmlNewNode (NULL, (xmlChar *)"part"); + xmlSetProp (node, (xmlChar *)"name", (xmlChar *) part->name); + + for (link = part->elements; link != NULL; link = g_list_next (link)) { + EFilterElement *element = link->data; + xmlNodePtr value; + + value = e_filter_element_xml_encode (element); + xmlAddChild (node, value); + } + + return node; +} + +gint +e_filter_part_xml_decode (EFilterPart *part, + xmlNodePtr node) +{ + xmlNodePtr child; + + g_return_val_if_fail (E_IS_FILTER_PART (part), -1); + g_return_val_if_fail (node != NULL, -1); + + for (child = node->children; child != NULL; child = child->next) { + EFilterElement *element; + xmlChar *name; + + if (strcmp ((gchar *) child->name, "value") != 0) + continue; + + name = xmlGetProp (child, (xmlChar *) "name"); + element = e_filter_part_find_element (part, (gchar *) name); + xmlFree (name); + + if (element != NULL) + e_filter_element_xml_decode (element, child); + } + + return 0; +} + +EFilterPart * +e_filter_part_clone (EFilterPart *part) +{ + EFilterPart *clone; + GList *link; + + g_return_val_if_fail (E_IS_FILTER_PART (part), NULL); + + clone = g_object_new (G_OBJECT_TYPE (part), NULL, NULL); + clone->name = g_strdup (part->name); + clone->title = g_strdup (part->title); + clone->code = g_strdup (part->code); + + for (link = part->elements; link != NULL; link = g_list_next (link)) { + EFilterElement *element = link->data; + EFilterElement *clone_element; + + clone_element = e_filter_element_clone (element); + clone->elements = g_list_append (clone->elements, clone_element); + } + + return clone; +} + +/* only copies values of matching parts in the right order */ +void +e_filter_part_copy_values (EFilterPart *dst_part, + EFilterPart *src_part) +{ + GList *dst_link, *src_link; + + g_return_if_fail (E_IS_FILTER_PART (dst_part)); + g_return_if_fail (E_IS_FILTER_PART (src_part)); + + /* NOTE: we go backwards, it just works better that way */ + + /* for each source type, search the dest type for + * a matching type in the same order */ + src_link = g_list_last (src_part->elements); + dst_link = g_list_last (dst_part->elements); + + while (src_link != NULL && dst_link != NULL) { + EFilterElement *src_element = src_link->data; + GList *link = dst_link; + + while (link != NULL) { + EFilterElement *dst_element = link->data; + GType dst_type = G_OBJECT_TYPE (dst_element); + GType src_type = G_OBJECT_TYPE (src_element); + + if (dst_type == src_type) { + e_filter_element_copy_value ( + dst_element, src_element); + dst_link = g_list_previous (link); + break; + } + + link = g_list_previous (link); + } + + src_link = g_list_previous (src_link); + } +} + +EFilterElement * +e_filter_part_find_element (EFilterPart *part, + const gchar *name) +{ + GList *link; + + g_return_val_if_fail (E_IS_FILTER_PART (part), NULL); + + if (name == NULL) + return NULL; + + for (link = part->elements; link != NULL; link = g_list_next (link)) { + EFilterElement *element = link->data; + + if (g_strcmp0 (element->name, name) == 0) + return element; + } + + return NULL; +} + +GtkWidget * +e_filter_part_get_widget (EFilterPart *part) +{ + GtkWidget *hbox; + GList *link; + + g_return_val_if_fail (E_IS_FILTER_PART (part), NULL); + + hbox = gtk_hbox_new (FALSE, 3); + + for (link = part->elements; link != NULL; link = g_list_next (link)) { + EFilterElement *element = link->data; + GtkWidget *widget; + + widget = e_filter_element_get_widget (element); + if (widget != NULL) + gtk_box_pack_start ( + GTK_BOX (hbox), widget, + E_IS_FILTER_FILE (element), + E_IS_FILTER_FILE (element), 3); + } + + gtk_widget_show_all (hbox); + + return hbox; +} + +/** + * e_filter_part_build_code: + * @part: + * @out: + * + * Outputs the code of a part. + **/ +void +e_filter_part_build_code (EFilterPart *part, + GString *out) +{ + GList *link; + + g_return_if_fail (E_IS_FILTER_PART (part)); + g_return_if_fail (out != NULL); + + if (part->code != NULL) + e_filter_part_expand_code (part, part->code, out); + + for (link = part->elements; link != NULL; link = g_list_next (link)) { + EFilterElement *element = link->data; + e_filter_element_build_code (element, out, part); + } +} + +/** + * e_filter_part_build_code_list: + * @l: + * @out: + * + * Construct a list of the filter parts code into + * a single string. + **/ +void +e_filter_part_build_code_list (GList *list, + GString *out) +{ + GList *link; + + g_return_if_fail (out != NULL); + + for (link = list; link != NULL; link = g_list_next (link)) { + EFilterPart *part = link->data; + + e_filter_part_build_code (part, out); + g_string_append (out, "\n "); + } +} + +/** + * e_filter_part_find_list: + * @l: + * @name: + * + * Find a filter part stored in a list. + * + * Return value: + **/ +EFilterPart * +e_filter_part_find_list (GList *list, + const gchar *name) +{ + GList *link; + + g_return_val_if_fail (name != NULL, NULL); + + for (link = list; link != NULL; link = g_list_next (link)) { + EFilterPart *part = link->data; + + if (g_strcmp0 (part->name, name) == 0) + return part; + } + + return NULL; +} + +/** + * e_filter_part_next_list: + * @l: + * @last: The last item retrieved, or NULL to start + * from the beginning of the list. + * + * Iterate through a filter part list. + * + * Return value: The next value in the list, or NULL if the + * list is expired. + **/ +EFilterPart * +e_filter_part_next_list (GList *list, + EFilterPart *last) +{ + GList *link = list; + + if (last != NULL) { + link = g_list_find (list, last); + if (link == NULL) + link = list; + else + link = link->next; + } + + return (link != NULL) ? link->data : NULL; +} + +/** + * e_filter_part_expand_code: + * @part: + * @str: + * @out: + * + * Expands the variables in string @str based on the values of the part. + **/ +void +e_filter_part_expand_code (EFilterPart *part, + const gchar *source, + GString *out) +{ + const gchar *newstart, *start, *end; + gchar *name = g_alloca (32); + gint len, namelen = 32; + + g_return_if_fail (E_IS_FILTER_PART (part)); + g_return_if_fail (source != NULL); + g_return_if_fail (out != NULL); + + start = source; + + while (start && (newstart = strstr (start, "${")) + && (end = strstr (newstart + 2, "}"))) { + EFilterElement *element; + + len = end - newstart - 2; + if (len + 1 > namelen) { + namelen = (len + 1) * 2; + name = g_alloca (namelen); + } + memcpy (name, newstart + 2, len); + name[len] = 0; + + element = e_filter_part_find_element (part, name); + if (element != NULL) { + g_string_append_printf (out, "%.*s", (gint)(newstart - start), start); + e_filter_element_format_sexp (element, out); +#if 0 + } else if ((val = g_hash_table_lookup (part->globals, name))) { + g_string_append_printf (out, "%.*s", newstart - start, start); + camel_sexp_encode_string (out, val); +#endif + } else { + g_string_append_printf (out, "%.*s", (gint)(end - start + 1), start); + } + start = end + 1; + } + + g_string_append (out, start); +} diff --git a/e-util/e-filter-part.h b/e-util/e-filter-part.h new file mode 100644 index 0000000000..b5ee2c46f2 --- /dev/null +++ b/e-util/e-filter-part.h @@ -0,0 +1,112 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_PART_H +#define E_FILTER_PART_H + +#include <gtk/gtk.h> +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include <e-util/e-alert.h> +#include <e-util/e-filter-element.h> + +/* Standard GObject macros */ +#define E_TYPE_FILTER_PART \ + (e_filter_part_get_type ()) +#define E_FILTER_PART(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_PART, EFilterPart)) +#define E_FILTER_PART_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_PART, EFilterPartClass)) +#define E_IS_FILTER_PART(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_PART)) +#define E_IS_FILTER_PART_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_PART)) +#define E_FILTER_PART_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_PART, EFilterPartClass)) + +G_BEGIN_DECLS + +struct _ERuleContext; + +typedef struct _EFilterPart EFilterPart; +typedef struct _EFilterPartClass EFilterPartClass; +typedef struct _EFilterPartPrivate EFilterPartPrivate; + +struct _EFilterPart { + GObject parent; + EFilterPartPrivate *priv; + + gchar *name; + gchar *title; + gchar *code; + GList *elements; +}; + +struct _EFilterPartClass { + GObjectClass parent_class; +}; + +GType e_filter_part_get_type (void); +EFilterPart * e_filter_part_new (void); +gboolean e_filter_part_validate (EFilterPart *part, + EAlert **alert); +gint e_filter_part_eq (EFilterPart *part_a, + EFilterPart *part_b); +gint e_filter_part_xml_create (EFilterPart *part, + xmlNodePtr node, + struct _ERuleContext *rc); +xmlNodePtr e_filter_part_xml_encode (EFilterPart *fe); +gint e_filter_part_xml_decode (EFilterPart *fe, + xmlNodePtr node); +EFilterPart * e_filter_part_clone (EFilterPart *part); +void e_filter_part_copy_values (EFilterPart *dst_part, + EFilterPart *src_part); +EFilterElement *e_filter_part_find_element (EFilterPart *part, + const gchar *name); +GtkWidget * e_filter_part_get_widget (EFilterPart *part); +void e_filter_part_build_code (EFilterPart *part, + GString *out); +void e_filter_part_expand_code (EFilterPart *part, + const gchar *str, + GString *out); + +void e_filter_part_build_code_list (GList *list, + GString *out); +EFilterPart * e_filter_part_find_list (GList *list, + const gchar *name); +EFilterPart * e_filter_part_next_list (GList *list, + EFilterPart *last); + +G_END_DECLS + +#endif /* E_FILTER_PART_H */ diff --git a/e-util/e-filter-rule.c b/e-util/e-filter-rule.c new file mode 100644 index 0000000000..111073bbad --- /dev/null +++ b/e-util/e-filter-rule.c @@ -0,0 +1,1241 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "e-alert-dialog.h" +#include "e-filter-rule.h" +#include "e-rule-context.h" + +#define E_FILTER_RULE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_FILTER_RULE, EFilterRulePrivate)) + +typedef struct _FilterPartData FilterPartData; +typedef struct _FilterRuleData FilterRuleData; + +struct _EFilterRulePrivate { + gint frozen; +}; + +struct _FilterPartData { + EFilterRule *rule; + ERuleContext *context; + EFilterPart *part; + GtkWidget *partwidget; + GtkWidget *container; +}; + +struct _FilterRuleData { + EFilterRule *rule; + ERuleContext *context; + GtkWidget *parts; +}; + +enum { + CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE ( + EFilterRule, + e_filter_rule, + G_TYPE_OBJECT) + +static void +filter_rule_grouping_changed_cb (GtkComboBox *combo_box, + EFilterRule *rule) +{ + rule->grouping = gtk_combo_box_get_active (combo_box); +} + +static void +filter_rule_threading_changed_cb (GtkComboBox *combo_box, + EFilterRule *rule) +{ + rule->threading = gtk_combo_box_get_active (combo_box); +} + +static void +part_combobox_changed (GtkComboBox *combobox, + FilterPartData *data) +{ + EFilterPart *part = NULL; + EFilterPart *newpart; + gint index, i; + + index = gtk_combo_box_get_active (combobox); + for (i = 0, part = e_rule_context_next_part (data->context, part); + part && i < index; + i++, part = e_rule_context_next_part (data->context, part)) { + /* traverse until reached index */ + } + + g_return_if_fail (part != NULL); + g_return_if_fail (i == index); + + /* dont update if we haven't changed */ + if (!strcmp (part->title, data->part->title)) + return; + + /* here we do a widget shuffle, throw away the old widget/rulepart, + * and create another */ + if (data->partwidget) + gtk_container_remove (GTK_CONTAINER (data->container), data->partwidget); + + newpart = e_filter_part_clone (part); + e_filter_part_copy_values (newpart, data->part); + e_filter_rule_replace_part (data->rule, data->part, newpart); + g_object_unref (data->part); + data->part = newpart; + data->partwidget = e_filter_part_get_widget (newpart); + if (data->partwidget) + gtk_box_pack_start ( + GTK_BOX (data->container), + data->partwidget, TRUE, TRUE, 0); +} + +static GtkWidget * +get_rule_part_widget (ERuleContext *context, + EFilterPart *newpart, + EFilterRule *rule) +{ + EFilterPart *part = NULL; + GtkWidget *combobox; + GtkWidget *hbox; + GtkWidget *p; + gint index = 0, current = 0; + FilterPartData *data; + + data = g_malloc0 (sizeof (*data)); + data->rule = rule; + data->context = context; + data->part = newpart; + + hbox = gtk_hbox_new (FALSE, 0); + /* only set to automatically clean up the memory */ + g_object_set_data_full ((GObject *) hbox, "data", data, g_free); + + p = e_filter_part_get_widget (newpart); + + data->partwidget = p; + data->container = hbox; + + combobox = gtk_combo_box_text_new (); + + /* sigh, this is a little ugly */ + while ((part = e_rule_context_next_part (context, part))) { + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (combobox), _(part->title)); + + if (!strcmp (newpart->title, part->title)) + current = index; + + index++; + } + + gtk_combo_box_set_active (GTK_COMBO_BOX (combobox), current); + g_signal_connect ( + combobox, "changed", + G_CALLBACK (part_combobox_changed), data); + gtk_widget_show (combobox); + + gtk_box_pack_start (GTK_BOX (hbox), combobox, FALSE, FALSE, 0); + if (p) + gtk_box_pack_start (GTK_BOX (hbox), p, TRUE, TRUE, 0); + + gtk_widget_show_all (hbox); + + return hbox; +} + +static void +less_parts (GtkWidget *button, + FilterRuleData *data) +{ + EFilterPart *part; + GtkWidget *rule; + FilterPartData *part_data; + + if (g_list_length (data->rule->parts) < 1) + return; + + rule = g_object_get_data ((GObject *) button, "rule"); + part_data = g_object_get_data ((GObject *) rule, "data"); + + g_return_if_fail (part_data != NULL); + + part = part_data->part; + + /* remove the part from the list */ + e_filter_rule_remove_part (data->rule, part); + g_object_unref (part); + + /* and from the display */ + gtk_container_remove (GTK_CONTAINER (data->parts), rule); + gtk_container_remove (GTK_CONTAINER (data->parts), button); +} + +static void +attach_rule (GtkWidget *rule, + FilterRuleData *data, + EFilterPart *part, + gint row) +{ + GtkWidget *remove; + + gtk_table_attach ( + GTK_TABLE (data->parts), rule, 0, 1, row, row + 1, + GTK_EXPAND | GTK_FILL, 0, 0, 0); + + remove = gtk_button_new_from_stock (GTK_STOCK_REMOVE); + g_object_set_data ((GObject *) remove, "rule", rule); + g_signal_connect ( + remove, "clicked", + G_CALLBACK (less_parts), data); + gtk_table_attach ( + GTK_TABLE (data->parts), remove, 1, 2, row, row + 1, + 0, 0, 0, 0); + + gtk_widget_show (remove); +} + +static void +do_grab_focus_cb (GtkWidget *widget, + gpointer data) +{ + gboolean *done = (gboolean *) data; + + if (*done || !widget) + return; + + if (gtk_widget_get_can_focus (widget) || GTK_IS_COMBO_BOX (widget)) { + *done = TRUE; + gtk_widget_grab_focus (widget); + } else if (GTK_IS_CONTAINER (widget)) { + gtk_container_foreach (GTK_CONTAINER (widget), do_grab_focus_cb, done); + } +} + +static void +more_parts (GtkWidget *button, + FilterRuleData *data) +{ + EFilterPart *new; + + /* first make sure that the last part is ok */ + if (data->rule->parts) { + EFilterPart *part; + GList *l; + EAlert *alert = NULL; + + l = g_list_last (data->rule->parts); + part = l->data; + if (!e_filter_part_validate (part, &alert)) { + GtkWidget *toplevel; + toplevel = gtk_widget_get_toplevel (button); + e_alert_run_dialog (GTK_WINDOW (toplevel), alert); + return; + } + } + + /* create a new rule entry, use the first type of rule */ + new = e_rule_context_next_part (data->context, NULL); + if (new) { + GtkWidget *w; + guint rows; + + new = e_filter_part_clone (new); + e_filter_rule_add_part (data->rule, new); + w = get_rule_part_widget (data->context, new, data->rule); + + g_object_get (data->parts, "n-rows", &rows, NULL); + gtk_table_resize (GTK_TABLE (data->parts), rows + 1, 2); + attach_rule (w, data, new, rows); + + if (GTK_IS_CONTAINER (w)) { + gboolean done = FALSE; + + gtk_container_foreach (GTK_CONTAINER (w), do_grab_focus_cb, &done); + } else + gtk_widget_grab_focus (w); + + /* also scroll down to see new part */ + w = (GtkWidget *) g_object_get_data (G_OBJECT (button), "scrolled-window"); + if (w) { + GtkAdjustment *adjustment; + + adjustment = gtk_scrolled_window_get_vadjustment ( + GTK_SCROLLED_WINDOW (w)); + if (adjustment) { + gdouble upper; + + upper = gtk_adjustment_get_upper (adjustment); + gtk_adjustment_set_value (adjustment, upper); + } + + } + } +} + +static void +name_changed (GtkEntry *entry, + EFilterRule *rule) +{ + g_free (rule->name); + rule->name = g_strdup (gtk_entry_get_text (entry)); +} + +GtkWidget * +e_filter_rule_get_widget (EFilterRule *rule, + ERuleContext *context) +{ + EFilterRuleClass *class; + + g_return_val_if_fail (E_IS_FILTER_RULE (rule), NULL); + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + + class = E_FILTER_RULE_GET_CLASS (rule); + g_return_val_if_fail (class->get_widget != NULL, NULL); + + return class->get_widget (rule, context); +} + +static void +filter_rule_load_set (xmlNodePtr node, + EFilterRule *rule, + ERuleContext *context) +{ + xmlNodePtr work; + gchar *rulename; + EFilterPart *part; + + work = node->children; + while (work) { + if (!strcmp ((gchar *) work->name, "part")) { + rulename = (gchar *) xmlGetProp (work, (xmlChar *)"name"); + part = e_rule_context_find_part (context, rulename); + if (part) { + part = e_filter_part_clone (part); + e_filter_part_xml_decode (part, work); + e_filter_rule_add_part (rule, part); + } else { + g_warning ("cannot find rule part '%s'\n", rulename); + } + xmlFree (rulename); + } else if (work->type == XML_ELEMENT_NODE) { + g_warning ("Unknown xml node in part: %s", work->name); + } + work = work->next; + } +} + +static void +filter_rule_finalize (GObject *object) +{ + EFilterRule *rule = E_FILTER_RULE (object); + + g_free (rule->name); + g_free (rule->source); + + g_list_foreach (rule->parts, (GFunc) g_object_unref, NULL); + g_list_free (rule->parts); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_filter_rule_parent_class)->finalize (object); +} + +static gint +filter_rule_validate (EFilterRule *rule, + EAlert **alert) +{ + gint valid = TRUE; + GList *parts; + + g_warn_if_fail (alert == NULL || *alert == NULL); + if (!rule->name || !*rule->name) { + if (alert) + *alert = e_alert_new ("filter:no-name", NULL); + + return FALSE; + } + + /* validate rule parts */ + parts = rule->parts; + valid = parts != NULL; + while (parts && valid) { + valid = e_filter_part_validate ((EFilterPart *) parts->data, alert); + parts = parts->next; + } + + return valid; +} + +static gint +filter_rule_eq (EFilterRule *rule_a, + EFilterRule *rule_b) +{ + GList *link_a; + GList *link_b; + + if (rule_a->enabled != rule_b->enabled) + return FALSE; + + if (rule_a->grouping != rule_b->grouping) + return FALSE; + + if (rule_a->threading != rule_b->threading) + return FALSE; + + if (g_strcmp0 (rule_a->name, rule_b->name) != 0) + return FALSE; + + if (g_strcmp0 (rule_a->source, rule_b->source) != 0) + return FALSE; + + link_a = rule_a->parts; + link_b = rule_b->parts; + + while (link_a != NULL && link_b != NULL) { + EFilterPart *part_a = link_a->data; + EFilterPart *part_b = link_b->data; + + if (!e_filter_part_eq (part_a, part_b)) + return FALSE; + + link_a = g_list_next (link_a); + link_b = g_list_next (link_b); + } + + if (link_a != NULL || link_b != NULL) + return FALSE; + + return TRUE; +} + +static xmlNodePtr +filter_rule_xml_encode (EFilterRule *rule) +{ + xmlNodePtr node, set, work; + GList *l; + + node = xmlNewNode (NULL, (xmlChar *)"rule"); + + xmlSetProp ( + node, (xmlChar *)"enabled", + (xmlChar *)(rule->enabled ? "true" : "false")); + + switch (rule->grouping) { + case E_FILTER_GROUP_ALL: + xmlSetProp (node, (xmlChar *)"grouping", (xmlChar *)"all"); + break; + case E_FILTER_GROUP_ANY: + xmlSetProp (node, (xmlChar *)"grouping", (xmlChar *)"any"); + break; + } + + switch (rule->threading) { + case E_FILTER_THREAD_NONE: + break; + case E_FILTER_THREAD_ALL: + xmlSetProp (node, (xmlChar *)"threading", (xmlChar *)"all"); + break; + case E_FILTER_THREAD_REPLIES: + xmlSetProp (node, (xmlChar *)"threading", (xmlChar *)"replies"); + break; + case E_FILTER_THREAD_REPLIES_PARENTS: + xmlSetProp (node, (xmlChar *)"threading", (xmlChar *)"replies_parents"); + break; + case E_FILTER_THREAD_SINGLE: + xmlSetProp (node, (xmlChar *)"threading", (xmlChar *)"single"); + break; + } + + if (rule->source) { + xmlSetProp (node, (xmlChar *)"source", (xmlChar *) rule->source); + } else { + /* set to the default filter type */ + xmlSetProp (node, (xmlChar *)"source", (xmlChar *)"incoming"); + } + + if (rule->name) { + gchar *escaped = g_markup_escape_text (rule->name, -1); + + work = xmlNewNode (NULL, (xmlChar *)"title"); + xmlNodeSetContent (work, (xmlChar *) escaped); + xmlAddChild (node, work); + + g_free (escaped); + } + + set = xmlNewNode (NULL, (xmlChar *)"partset"); + xmlAddChild (node, set); + l = rule->parts; + while (l) { + work = e_filter_part_xml_encode ((EFilterPart *) l->data); + xmlAddChild (set, work); + l = l->next; + } + + return node; +} + +static gint +filter_rule_xml_decode (EFilterRule *rule, + xmlNodePtr node, + ERuleContext *context) +{ + xmlNodePtr work; + gchar *grouping; + gchar *source; + + g_free (rule->name); + rule->name = NULL; + + grouping = (gchar *) xmlGetProp (node, (xmlChar *)"enabled"); + if (!grouping) + rule->enabled = TRUE; + else { + rule->enabled = strcmp (grouping, "false") != 0; + xmlFree (grouping); + } + + grouping = (gchar *) xmlGetProp (node, (xmlChar *)"grouping"); + if (!strcmp (grouping, "any")) + rule->grouping = E_FILTER_GROUP_ANY; + else + rule->grouping = E_FILTER_GROUP_ALL; + xmlFree (grouping); + + rule->threading = E_FILTER_THREAD_NONE; + if (context->flags & E_RULE_CONTEXT_THREADING + && (grouping = (gchar *) xmlGetProp (node, (xmlChar *)"threading"))) { + if (!strcmp (grouping, "all")) + rule->threading = E_FILTER_THREAD_ALL; + else if (!strcmp (grouping, "replies")) + rule->threading = E_FILTER_THREAD_REPLIES; + else if (!strcmp (grouping, "replies_parents")) + rule->threading = E_FILTER_THREAD_REPLIES_PARENTS; + else if (!strcmp (grouping, "single")) + rule->threading = E_FILTER_THREAD_SINGLE; + xmlFree (grouping); + } + + g_free (rule->source); + source = (gchar *) xmlGetProp (node, (xmlChar *)"source"); + if (source) { + rule->source = g_strdup (source); + xmlFree (source); + } else { + /* default filter type */ + rule->source = g_strdup ("incoming"); + } + + work = node->children; + while (work) { + if (!strcmp ((gchar *) work->name, "partset")) { + filter_rule_load_set (work, rule, context); + } else if (!strcmp ((gchar *) work->name, "title") || + !strcmp ((gchar *) work->name, "_title")) { + + if (!rule->name) { + gchar *str, *decstr = NULL; + + str = (gchar *) xmlNodeGetContent (work); + if (str) { + decstr = g_strdup (_(str)); + xmlFree (str); + } + rule->name = decstr; + } + } + work = work->next; + } + + return 0; +} + +static void +filter_rule_build_code (EFilterRule *rule, + GString *out) +{ + switch (rule->threading) { + case E_FILTER_THREAD_NONE: + break; + case E_FILTER_THREAD_ALL: + g_string_append (out, " (match-threads \"all\" "); + break; + case E_FILTER_THREAD_REPLIES: + g_string_append (out, " (match-threads \"replies\" "); + break; + case E_FILTER_THREAD_REPLIES_PARENTS: + g_string_append (out, " (match-threads \"replies_parents\" "); + break; + case E_FILTER_THREAD_SINGLE: + g_string_append (out, " (match-threads \"single\" "); + break; + } + + switch (rule->grouping) { + case E_FILTER_GROUP_ALL: + g_string_append (out, " (and\n "); + break; + case E_FILTER_GROUP_ANY: + g_string_append (out, " (or\n "); + break; + default: + g_warning ("Invalid grouping"); + } + + e_filter_part_build_code_list (rule->parts, out); + g_string_append (out, ")\n"); + + if (rule->threading != E_FILTER_THREAD_NONE) + g_string_append (out, ")\n"); +} + +static void +filter_rule_copy (EFilterRule *dest, + EFilterRule *src) +{ + GList *node; + + dest->enabled = src->enabled; + + g_free (dest->name); + dest->name = g_strdup (src->name); + + g_free (dest->source); + dest->source = g_strdup (src->source); + + dest->grouping = src->grouping; + dest->threading = src->threading; + + if (dest->parts) { + g_list_foreach (dest->parts, (GFunc) g_object_unref, NULL); + g_list_free (dest->parts); + dest->parts = NULL; + } + + node = src->parts; + while (node) { + EFilterPart *part; + + part = e_filter_part_clone (node->data); + dest->parts = g_list_append (dest->parts, part); + node = node->next; + } +} + +static void +ensure_scrolled_width_cb (GtkAdjustment *adj, + GParamSpec *param_spec, + GtkScrolledWindow *scrolled_window) +{ + gtk_scrolled_window_set_min_content_width ( + scrolled_window, + gtk_adjustment_get_upper (adj)); +} + +static void +ensure_scrolled_height_cb (GtkAdjustment *adj, + GParamSpec *param_spec, + GtkScrolledWindow *scrolled_window) +{ + GtkWidget *toplevel; + GdkScreen *screen; + gint toplevel_height, scw_height, require_scw_height = 0, max_height; + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (scrolled_window)); + if (!toplevel || !gtk_widget_is_toplevel (toplevel)) + return; + + scw_height = gtk_widget_get_allocated_height (GTK_WIDGET (scrolled_window)); + + gtk_widget_get_preferred_height_for_width (gtk_bin_get_child (GTK_BIN (scrolled_window)), + gtk_widget_get_allocated_width (GTK_WIDGET (scrolled_window)), + &require_scw_height, NULL); + + if (scw_height >= require_scw_height) { + if (require_scw_height > 0) + gtk_scrolled_window_set_min_content_height (scrolled_window, require_scw_height); + return; + } + + if (!GTK_IS_WINDOW (toplevel) || + !gtk_widget_get_window (toplevel)) + return; + + screen = gtk_window_get_screen (GTK_WINDOW (toplevel)); + if (screen) { + gint monitor; + GdkRectangle workarea; + + monitor = gdk_screen_get_monitor_at_window (screen, gtk_widget_get_window (toplevel)); + if (monitor < 0) + monitor = 0; + + gdk_screen_get_monitor_workarea (screen, monitor, &workarea); + + /* can enlarge up to 4 / 5 monitor's work area height */ + max_height = workarea.height * 4 / 5; + } else { + return; + } + + toplevel_height = gtk_widget_get_allocated_height (toplevel); + if (toplevel_height + require_scw_height - scw_height > max_height) + return; + + gtk_scrolled_window_set_min_content_height (scrolled_window, require_scw_height); +} + +static GtkWidget * +filter_rule_get_widget (EFilterRule *rule, + ERuleContext *context) +{ + GtkGrid *hgrid, *vgrid, *inframe; + GtkWidget *parts, *add, *label, *name, *w; + GtkWidget *combobox; + GtkWidget *scrolledwindow; + GtkAdjustment *hadj, *vadj; + GList *l; + gchar *text; + EFilterPart *part; + FilterRuleData *data; + gint rows, i; + + /* this stuff should probably be a table, but the + * rule parts need to be a vbox */ + vgrid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (vgrid, 6); + gtk_orientable_set_orientation (GTK_ORIENTABLE (vgrid), GTK_ORIENTATION_VERTICAL); + + label = gtk_label_new_with_mnemonic (_("R_ule name:")); + name = gtk_entry_new (); + gtk_widget_set_hexpand (name, TRUE); + gtk_widget_set_halign (name, GTK_ALIGN_FILL); + gtk_label_set_mnemonic_widget ((GtkLabel *) label, name); + + if (!rule->name) { + rule->name = g_strdup (_("Untitled")); + gtk_entry_set_text (GTK_ENTRY (name), rule->name); + /* FIXME: do we want the following code in the future? */ + /*gtk_editable_select_region (GTK_EDITABLE (name), 0, -1);*/ + } else { + gtk_entry_set_text (GTK_ENTRY (name), rule->name); + } + + g_signal_connect ( + name, "realize", + G_CALLBACK (gtk_widget_grab_focus), name); + + hgrid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_column_spacing (hgrid, 12); + + gtk_grid_attach (hgrid, label, 0, 0, 1, 1); + gtk_grid_attach_next_to (hgrid, name, label, GTK_POS_RIGHT, 1, 1); + + gtk_container_add (GTK_CONTAINER (vgrid), GTK_WIDGET (hgrid)); + + g_signal_connect ( + name, "changed", + G_CALLBACK (name_changed), rule); + + hgrid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_column_spacing (hgrid, 12); + gtk_container_add (GTK_CONTAINER (vgrid), GTK_WIDGET (hgrid)); + + /* this is the parts table, it should probably be inside a scrolling list */ + rows = g_list_length (rule->parts); + parts = gtk_table_new (rows, 2, FALSE); + + /* data for the parts part of the display */ + data = g_malloc0 (sizeof (*data)); + data->context = context; + data->rule = rule; + data->parts = parts; + + /* only set to automatically clean up the memory */ + g_object_set_data_full ((GObject *) vgrid, "data", data, g_free); + + if (context->flags & E_RULE_CONTEXT_GROUPING) { + const gchar *thread_types[] = { + N_("all the following conditions"), + N_("any of the following conditions") + }; + + hgrid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_column_spacing (hgrid, 12); + + label = gtk_label_new_with_mnemonic (_("_Find items which match:")); + combobox = gtk_combo_box_text_new (); + + for (i = 0; i < 2; i++) { + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (combobox), + _(thread_types[i])); + } + + gtk_label_set_mnemonic_widget ((GtkLabel *) label, combobox); + gtk_combo_box_set_active (GTK_COMBO_BOX (combobox), rule->grouping); + + gtk_grid_attach (hgrid, label, 0, 0, 1, 1); + gtk_grid_attach_next_to (hgrid, combobox, label, GTK_POS_RIGHT, 1, 1); + + g_signal_connect ( + combobox, "changed", + G_CALLBACK (filter_rule_grouping_changed_cb), rule); + + gtk_container_add (GTK_CONTAINER (vgrid), GTK_WIDGET (hgrid)); + } else { + text = g_strdup_printf ( + "<b>%s</b>", + _("Find items that meet the following conditions")); + label = gtk_label_new (text); + gtk_label_set_use_markup (GTK_LABEL (label), TRUE); + gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5); + gtk_container_add (GTK_CONTAINER (vgrid), label); + g_free (text); + } + + hgrid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_column_spacing (hgrid, 12); + + if (context->flags & E_RULE_CONTEXT_THREADING) { + const gchar *thread_types[] = { + /* Translators: "None" for not including threads; + * part of "Include threads: None" */ + N_("None"), + N_("All related"), + N_("Replies"), + N_("Replies and parents"), + N_("No reply or parent") + }; + + label = gtk_label_new_with_mnemonic (_("I_nclude threads:")); + combobox = gtk_combo_box_text_new (); + + for (i = 0; i < 5; i++) { + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (combobox), + _(thread_types[i])); + } + + gtk_label_set_mnemonic_widget ((GtkLabel *) label, combobox); + gtk_combo_box_set_active (GTK_COMBO_BOX (combobox), rule->threading); + + gtk_grid_attach (hgrid, label, 0, 0, 1, 1); + gtk_grid_attach_next_to (hgrid, combobox, label, GTK_POS_RIGHT, 1, 1); + + g_signal_connect ( + combobox, "changed", + G_CALLBACK (filter_rule_threading_changed_cb), rule); + } + + gtk_container_add (GTK_CONTAINER (vgrid), GTK_WIDGET (hgrid)); + + hgrid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_column_spacing (hgrid, 3); + gtk_widget_set_vexpand (GTK_WIDGET (hgrid), TRUE); + gtk_widget_set_valign (GTK_WIDGET (hgrid), GTK_ALIGN_FILL); + + gtk_container_add (GTK_CONTAINER (vgrid), GTK_WIDGET (hgrid)); + + label = gtk_label_new (""); + gtk_grid_attach (hgrid, label, 0, 0, 1, 1); + + inframe = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (inframe, 6); + gtk_orientable_set_orientation (GTK_ORIENTABLE (inframe), GTK_ORIENTATION_VERTICAL); + gtk_widget_set_hexpand (GTK_WIDGET (inframe), TRUE); + gtk_widget_set_halign (GTK_WIDGET (inframe), GTK_ALIGN_FILL); + gtk_widget_set_vexpand (GTK_WIDGET (inframe), TRUE); + gtk_widget_set_valign (GTK_WIDGET (inframe), GTK_ALIGN_FILL); + gtk_grid_attach_next_to (hgrid, GTK_WIDGET (inframe), label, GTK_POS_RIGHT, 1, 1); + + l = rule->parts; + i = 0; + while (l) { + part = l->data; + w = get_rule_part_widget (context, part, rule); + attach_rule (w, data, part, i++); + l = g_list_next (l); + } + + hadj = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 1.0, 1.0, 1.0, 1.0)); + vadj = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 1.0, 1.0, 1.0, 1.0)); + scrolledwindow = gtk_scrolled_window_new (hadj, vadj); + + g_signal_connect ( + hadj, "notify::upper", + G_CALLBACK (ensure_scrolled_width_cb), scrolledwindow); + g_signal_connect ( + vadj, "notify::upper", + G_CALLBACK (ensure_scrolled_height_cb), scrolledwindow); + + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (scrolledwindow), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + + gtk_scrolled_window_add_with_viewport ( + GTK_SCROLLED_WINDOW (scrolledwindow), parts); + + gtk_widget_set_vexpand (scrolledwindow, TRUE); + gtk_widget_set_valign (scrolledwindow, GTK_ALIGN_FILL); + gtk_widget_set_hexpand (scrolledwindow, TRUE); + gtk_widget_set_halign (scrolledwindow, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (inframe), scrolledwindow); + + hgrid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_column_spacing (hgrid, 3); + + add = gtk_button_new_with_mnemonic (_("A_dd Condition")); + gtk_button_set_image ( + GTK_BUTTON (add), gtk_image_new_from_stock ( + GTK_STOCK_ADD, GTK_ICON_SIZE_BUTTON)); + g_signal_connect ( + add, "clicked", + G_CALLBACK (more_parts), data); + gtk_grid_attach (hgrid, add, 0, 0, 1, 1); + + gtk_container_add (GTK_CONTAINER (inframe), GTK_WIDGET (hgrid)); + + gtk_widget_show_all (GTK_WIDGET (vgrid)); + + g_object_set_data (G_OBJECT (add), "scrolled-window", scrolledwindow); + + return GTK_WIDGET (vgrid); +} + +static void +e_filter_rule_class_init (EFilterRuleClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EFilterRulePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = filter_rule_finalize; + + class->validate = filter_rule_validate; + class->eq = filter_rule_eq; + class->xml_encode = filter_rule_xml_encode; + class->xml_decode = filter_rule_xml_decode; + class->build_code = filter_rule_build_code; + class->copy = filter_rule_copy; + class->get_widget = filter_rule_get_widget; + + signals[CHANGED] = g_signal_new ( + "changed", + E_TYPE_FILTER_RULE, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EFilterRuleClass, changed), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_filter_rule_init (EFilterRule *rule) +{ + rule->priv = E_FILTER_RULE_GET_PRIVATE (rule); + rule->enabled = TRUE; +} + +/** + * filter_rule_new: + * + * Create a new EFilterRule object. + * + * Return value: A new #EFilterRule object. + **/ +EFilterRule * +e_filter_rule_new (void) +{ + return g_object_new (E_TYPE_FILTER_RULE, NULL); +} + +EFilterRule * +e_filter_rule_clone (EFilterRule *rule) +{ + EFilterRule *clone; + + g_return_val_if_fail (E_IS_FILTER_RULE (rule), NULL); + + clone = g_object_new (G_OBJECT_TYPE (rule), NULL); + e_filter_rule_copy (clone, rule); + + return clone; +} + +void +e_filter_rule_set_name (EFilterRule *rule, + const gchar *name) +{ + g_return_if_fail (E_IS_FILTER_RULE (rule)); + + if (g_strcmp0 (rule->name, name) == 0) + return; + + g_free (rule->name); + rule->name = g_strdup (name); + + e_filter_rule_emit_changed (rule); +} + +void +e_filter_rule_set_source (EFilterRule *rule, + const gchar *source) +{ + g_return_if_fail (E_IS_FILTER_RULE (rule)); + + if (g_strcmp0 (rule->source, source) == 0) + return; + + g_free (rule->source); + rule->source = g_strdup (source); + + e_filter_rule_emit_changed (rule); +} + +gint +e_filter_rule_validate (EFilterRule *rule, + EAlert **alert) +{ + EFilterRuleClass *class; + + g_return_val_if_fail (E_IS_FILTER_RULE (rule), FALSE); + + class = E_FILTER_RULE_GET_CLASS (rule); + g_return_val_if_fail (class->validate != NULL, FALSE); + + return class->validate (rule, alert); +} + +gint +e_filter_rule_eq (EFilterRule *rule_a, + EFilterRule *rule_b) +{ + EFilterRuleClass *class; + + g_return_val_if_fail (E_IS_FILTER_RULE (rule_a), FALSE); + g_return_val_if_fail (E_IS_FILTER_RULE (rule_b), FALSE); + + class = E_FILTER_RULE_GET_CLASS (rule_a); + g_return_val_if_fail (class->eq != NULL, FALSE); + + if (G_OBJECT_TYPE (rule_a) != G_OBJECT_TYPE (rule_b)) + return FALSE; + + return class->eq (rule_a, rule_b); +} + +xmlNodePtr +e_filter_rule_xml_encode (EFilterRule *rule) +{ + EFilterRuleClass *class; + + g_return_val_if_fail (E_IS_FILTER_RULE (rule), NULL); + + class = E_FILTER_RULE_GET_CLASS (rule); + g_return_val_if_fail (class->xml_encode != NULL, NULL); + + return class->xml_encode (rule); +} + +gint +e_filter_rule_xml_decode (EFilterRule *rule, + xmlNodePtr node, + ERuleContext *context) +{ + EFilterRuleClass *class; + gint result; + + g_return_val_if_fail (E_IS_FILTER_RULE (rule), FALSE); + g_return_val_if_fail (node != NULL, FALSE); + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), FALSE); + + class = E_FILTER_RULE_GET_CLASS (rule); + g_return_val_if_fail (class->xml_decode != NULL, FALSE); + + rule->priv->frozen++; + result = class->xml_decode (rule, node, context); + rule->priv->frozen--; + + e_filter_rule_emit_changed (rule); + + return result; +} + +void +e_filter_rule_copy (EFilterRule *dst_rule, + EFilterRule *src_rule) +{ + EFilterRuleClass *class; + + g_return_if_fail (E_IS_FILTER_RULE (dst_rule)); + g_return_if_fail (E_IS_FILTER_RULE (src_rule)); + + class = E_FILTER_RULE_GET_CLASS (dst_rule); + g_return_if_fail (class->copy != NULL); + + class->copy (dst_rule, src_rule); + + e_filter_rule_emit_changed (dst_rule); +} + +void +e_filter_rule_add_part (EFilterRule *rule, + EFilterPart *part) +{ + g_return_if_fail (E_IS_FILTER_RULE (rule)); + g_return_if_fail (E_IS_FILTER_PART (part)); + + rule->parts = g_list_append (rule->parts, part); + + e_filter_rule_emit_changed (rule); +} + +void +e_filter_rule_remove_part (EFilterRule *rule, + EFilterPart *part) +{ + g_return_if_fail (E_IS_FILTER_RULE (rule)); + g_return_if_fail (E_IS_FILTER_PART (part)); + + rule->parts = g_list_remove (rule->parts, part); + + e_filter_rule_emit_changed (rule); +} + +void +e_filter_rule_replace_part (EFilterRule *rule, + EFilterPart *old_part, + EFilterPart *new_part) +{ + GList *link; + + g_return_if_fail (E_IS_FILTER_RULE (rule)); + g_return_if_fail (E_IS_FILTER_PART (old_part)); + g_return_if_fail (E_IS_FILTER_PART (new_part)); + + link = g_list_find (rule->parts, old_part); + if (link != NULL) + link->data = new_part; + else + rule->parts = g_list_append (rule->parts, new_part); + + e_filter_rule_emit_changed (rule); +} + +void +e_filter_rule_build_code (EFilterRule *rule, + GString *out) +{ + EFilterRuleClass *class; + + g_return_if_fail (E_IS_FILTER_RULE (rule)); + g_return_if_fail (out != NULL); + + class = E_FILTER_RULE_GET_CLASS (rule); + g_return_if_fail (class->build_code != NULL); + + class->build_code (rule, out); +} + +void +e_filter_rule_emit_changed (EFilterRule *rule) +{ + g_return_if_fail (E_IS_FILTER_RULE (rule)); + + if (rule->priv->frozen == 0) + g_signal_emit (rule, signals[CHANGED], 0); +} + +EFilterRule * +e_filter_rule_next_list (GList *list, + EFilterRule *last, + const gchar *source) +{ + GList *link = list; + + if (last != NULL) { + link = g_list_find (link, last); + if (link == NULL) + link = list; + else + link = g_list_next (link); + } + + if (source != NULL) { + while (link != NULL) { + EFilterRule *rule = link->data; + + if (g_strcmp0 (rule->source, source) == 0) + break; + + link = g_list_next (link); + } + } + + return (link != NULL) ? link->data : NULL; +} + +EFilterRule * +e_filter_rule_find_list (GList *list, + const gchar *name, + const gchar *source) +{ + GList *link; + + g_return_val_if_fail (name != NULL, FALSE); + + for (link = list; link != NULL; link = g_list_next (link)) { + EFilterRule *rule = link->data; + + if (strcmp (rule->name, name) == 0) + if (source == NULL || (rule->source != NULL && + strcmp (rule->source, source) == 0)) + return rule; + } + + return NULL; +} + +#ifdef FOR_TRANSLATIONS_ONLY + +static gchar *list[] = { + N_("Incoming"), N_("Outgoing") +}; +#endif diff --git a/e-util/e-filter-rule.h b/e-util/e-filter-rule.h new file mode 100644 index 0000000000..8670265d88 --- /dev/null +++ b/e-util/e-filter-rule.h @@ -0,0 +1,163 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FILTER_RULE_H +#define E_FILTER_RULE_H + +#include <e-util/e-filter-part.h> + +/* Standard GObject macros */ +#define E_TYPE_FILTER_RULE \ + (e_filter_rule_get_type ()) +#define E_FILTER_RULE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FILTER_RULE, EFilterRule)) +#define E_FILTER_RULE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FILTER_RULE, EFilterRuleClass)) +#define E_IS_FILTER_RULE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FILTER_RULE)) +#define E_IS_FILTER_RULE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FILTER_RULE)) +#define E_FILTER_RULE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FILTER_RULE, EFilterRuleClass)) + +G_BEGIN_DECLS + +struct _RuleContext; + +typedef struct _EFilterRule EFilterRule; +typedef struct _EFilterRuleClass EFilterRuleClass; +typedef struct _EFilterRulePrivate EFilterRulePrivate; + +enum _filter_grouping_t { + E_FILTER_GROUP_ALL, /* all rules must match */ + E_FILTER_GROUP_ANY /* any rule must match */ +}; + +/* threading, if the context supports it */ +enum _filter_threading_t { + E_FILTER_THREAD_NONE, /* don't add any thread matching */ + E_FILTER_THREAD_ALL, /* add all possible threads */ + E_FILTER_THREAD_REPLIES, /* add only replies */ + E_FILTER_THREAD_REPLIES_PARENTS, /* replies plus parents */ + E_FILTER_THREAD_SINGLE /* messages with no replies or parents */ +}; + +#define E_FILTER_SOURCE_INCOMING "incoming" /* performed on incoming email */ +#define E_FILTER_SOURCE_DEMAND "demand" /* performed on the selected folder + * when the user asks for it */ +#define E_FILTER_SOURCE_OUTGOING "outgoing"/* performed on outgoing mail */ +#define E_FILTER_SOURCE_JUNKTEST "junktest"/* check incoming mail for junk */ + +struct _EFilterRule { + GObject parent_object; + EFilterRulePrivate *priv; + + gchar *name; + gchar *source; + + enum _filter_grouping_t grouping; + enum _filter_threading_t threading; + + guint system:1; /* this is a system rule, cannot be edited/deleted */ + GList *parts; + + gboolean enabled; +}; + +struct _EFilterRuleClass { + GObjectClass parent_class; + + /* virtual methods */ + gint (*validate) (EFilterRule *rule, + EAlert **alert); + gint (*eq) (EFilterRule *rule_a, + EFilterRule *rule_b); + + xmlNodePtr (*xml_encode) (EFilterRule *rule); + gint (*xml_decode) (EFilterRule *rule, + xmlNodePtr node, + struct _ERuleContext *context); + + void (*build_code) (EFilterRule *rule, + GString *out); + + void (*copy) (EFilterRule *dst_rule, + EFilterRule *src_rule); + + GtkWidget * (*get_widget) (EFilterRule *rule, + struct _ERuleContext *context); + + /* signals */ + void (*changed) (EFilterRule *rule); +}; + +GType e_filter_rule_get_type (void); +EFilterRule * e_filter_rule_new (void); +EFilterRule * e_filter_rule_clone (EFilterRule *rule); +void e_filter_rule_set_name (EFilterRule *rule, + const gchar *name); +void e_filter_rule_set_source (EFilterRule *rule, + const gchar *source); +gint e_filter_rule_validate (EFilterRule *rule, + EAlert **alert); +gint e_filter_rule_eq (EFilterRule *rule_a, + EFilterRule *rule_b); +xmlNodePtr e_filter_rule_xml_encode (EFilterRule *rule); +gint e_filter_rule_xml_decode (EFilterRule *rule, + xmlNodePtr node, + struct _ERuleContext *context); +void e_filter_rule_copy (EFilterRule *dst_rule, + EFilterRule *src_rule); +void e_filter_rule_add_part (EFilterRule *rule, + EFilterPart *part); +void e_filter_rule_remove_part (EFilterRule *rule, + EFilterPart *part); +void e_filter_rule_replace_part (EFilterRule *rule, + EFilterPart *old_part, + EFilterPart *new_part); +GtkWidget * e_filter_rule_get_widget (EFilterRule *rule, + struct _ERuleContext *context); +void e_filter_rule_build_code (EFilterRule *rule, + GString *out); +void e_filter_rule_emit_changed (EFilterRule *rule); + +/* static functions */ +EFilterRule * e_filter_rule_next_list (GList *list, + EFilterRule *last, + const gchar *source); +EFilterRule * e_filter_rule_find_list (GList *list, + const gchar *name, + const gchar *source); + +G_END_DECLS + +#endif /* E_FILTER_RULE_H */ diff --git a/e-util/e-focus-tracker.c b/e-util/e-focus-tracker.c new file mode 100644 index 0000000000..a610605987 --- /dev/null +++ b/e-util/e-focus-tracker.c @@ -0,0 +1,886 @@ +/* + * e-focus-tracker.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-focus-tracker.h" + +#include <glib/gi18n-lib.h> + +#include "e-selectable.h" + +#define E_FOCUS_TRACKER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_FOCUS_TRACKER, EFocusTrackerPrivate)) + +struct _EFocusTrackerPrivate { + GtkWidget *focus; /* not referenced */ + GtkWindow *window; + + GtkAction *cut_clipboard; + GtkAction *copy_clipboard; + GtkAction *paste_clipboard; + GtkAction *delete_selection; + GtkAction *select_all; +}; + +enum { + PROP_0, + PROP_FOCUS, + PROP_WINDOW, + PROP_CUT_CLIPBOARD_ACTION, + PROP_COPY_CLIPBOARD_ACTION, + PROP_PASTE_CLIPBOARD_ACTION, + PROP_DELETE_SELECTION_ACTION, + PROP_SELECT_ALL_ACTION +}; + +G_DEFINE_TYPE ( + EFocusTracker, + e_focus_tracker, + G_TYPE_OBJECT) + +static void +focus_tracker_disable_actions (EFocusTracker *focus_tracker) +{ + GtkAction *action; + + action = e_focus_tracker_get_cut_clipboard_action (focus_tracker); + if (action != NULL) + gtk_action_set_sensitive (action, FALSE); + + action = e_focus_tracker_get_copy_clipboard_action (focus_tracker); + if (action != NULL) + gtk_action_set_sensitive (action, FALSE); + + action = e_focus_tracker_get_paste_clipboard_action (focus_tracker); + if (action != NULL) + gtk_action_set_sensitive (action, FALSE); + + action = e_focus_tracker_get_delete_selection_action (focus_tracker); + if (action != NULL) + gtk_action_set_sensitive (action, FALSE); + + action = e_focus_tracker_get_select_all_action (focus_tracker); + if (action != NULL) + gtk_action_set_sensitive (action, FALSE); +} + +static void +focus_tracker_editable_update_actions (EFocusTracker *focus_tracker, + GtkEditable *editable, + GdkAtom *targets, + gint n_targets) +{ + GtkAction *action; + gboolean can_edit_text; + gboolean clipboard_has_text; + gboolean text_is_selected; + gboolean sensitive; + + can_edit_text = + gtk_editable_get_editable (editable); + + clipboard_has_text = (targets != NULL) && + gtk_targets_include_text (targets, n_targets); + + text_is_selected = + gtk_editable_get_selection_bounds (editable, NULL, NULL); + + action = e_focus_tracker_get_cut_clipboard_action (focus_tracker); + if (action != NULL) { + sensitive = can_edit_text && text_is_selected; + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, _("Cut the selection")); + } + + action = e_focus_tracker_get_copy_clipboard_action (focus_tracker); + if (action != NULL) { + sensitive = text_is_selected; + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, _("Copy the selection")); + } + + action = e_focus_tracker_get_paste_clipboard_action (focus_tracker); + if (action != NULL) { + sensitive = can_edit_text && clipboard_has_text; + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, _("Paste the clipboard")); + } + + action = e_focus_tracker_get_delete_selection_action (focus_tracker); + if (action != NULL) { + sensitive = can_edit_text && text_is_selected; + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, _("Delete the selection")); + } + + action = e_focus_tracker_get_select_all_action (focus_tracker); + if (action != NULL) { + sensitive = TRUE; /* always enabled */ + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, _("Select all text")); + } +} + +static void +focus_tracker_selectable_update_actions (EFocusTracker *focus_tracker, + ESelectable *selectable, + GdkAtom *targets, + gint n_targets) +{ + ESelectableInterface *interface; + GtkAction *action; + + interface = E_SELECTABLE_GET_INTERFACE (selectable); + + e_selectable_update_actions ( + selectable, focus_tracker, targets, n_targets); + + /* Disable actions for which the corresponding method is not + * implemented. This allows update_actions() implementations + * to simply skip the actions they don't support, which in turn + * allows us to add new actions without disturbing the existing + * ESelectable implementations. */ + + action = e_focus_tracker_get_cut_clipboard_action (focus_tracker); + if (action != NULL && interface->cut_clipboard == NULL) + gtk_action_set_sensitive (action, FALSE); + + action = e_focus_tracker_get_copy_clipboard_action (focus_tracker); + if (action != NULL && interface->copy_clipboard == NULL) + gtk_action_set_sensitive (action, FALSE); + + action = e_focus_tracker_get_paste_clipboard_action (focus_tracker); + if (action != NULL && interface->paste_clipboard == NULL) + gtk_action_set_sensitive (action, FALSE); + + action = e_focus_tracker_get_delete_selection_action (focus_tracker); + if (action != NULL && interface->delete_selection == NULL) + gtk_action_set_sensitive (action, FALSE); + + action = e_focus_tracker_get_select_all_action (focus_tracker); + if (action != NULL && interface->select_all == NULL) + gtk_action_set_sensitive (action, FALSE); +} + +static void +focus_tracker_targets_received_cb (GtkClipboard *clipboard, + GdkAtom *targets, + gint n_targets, + EFocusTracker *focus_tracker) +{ + GtkWidget *focus; + + focus = e_focus_tracker_get_focus (focus_tracker); + + if (focus == NULL) + focus_tracker_disable_actions (focus_tracker); + + else if (GTK_IS_EDITABLE (focus)) + focus_tracker_editable_update_actions ( + focus_tracker, GTK_EDITABLE (focus), + targets, n_targets); + + else if (E_IS_SELECTABLE (focus)) + focus_tracker_selectable_update_actions ( + focus_tracker, E_SELECTABLE (focus), + targets, n_targets); + + g_object_unref (focus_tracker); +} + +static void +focus_tracker_set_focus_cb (GtkWindow *window, + GtkWidget *focus, + EFocusTracker *focus_tracker) +{ + while (focus != NULL) { + if (GTK_IS_EDITABLE (focus)) + break; + + if (E_IS_SELECTABLE (focus)) + break; + + focus = gtk_widget_get_parent (focus); + } + + if (focus == focus_tracker->priv->focus) + return; + + focus_tracker->priv->focus = focus; + g_object_notify (G_OBJECT (focus_tracker), "focus"); + + e_focus_tracker_update_actions (focus_tracker); +} + +static void +focus_tracker_set_window (EFocusTracker *focus_tracker, + GtkWindow *window) +{ + g_return_if_fail (GTK_IS_WINDOW (window)); + g_return_if_fail (focus_tracker->priv->window == NULL); + + focus_tracker->priv->window = g_object_ref (window); + + g_signal_connect ( + window, "set-focus", + G_CALLBACK (focus_tracker_set_focus_cb), focus_tracker); +} + +static void +focus_tracker_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_WINDOW: + focus_tracker_set_window ( + E_FOCUS_TRACKER (object), + g_value_get_object (value)); + return; + + case PROP_CUT_CLIPBOARD_ACTION: + e_focus_tracker_set_cut_clipboard_action ( + E_FOCUS_TRACKER (object), + g_value_get_object (value)); + return; + + case PROP_COPY_CLIPBOARD_ACTION: + e_focus_tracker_set_copy_clipboard_action ( + E_FOCUS_TRACKER (object), + g_value_get_object (value)); + return; + + case PROP_PASTE_CLIPBOARD_ACTION: + e_focus_tracker_set_paste_clipboard_action ( + E_FOCUS_TRACKER (object), + g_value_get_object (value)); + return; + + case PROP_DELETE_SELECTION_ACTION: + e_focus_tracker_set_delete_selection_action ( + E_FOCUS_TRACKER (object), + g_value_get_object (value)); + return; + + case PROP_SELECT_ALL_ACTION: + e_focus_tracker_set_select_all_action ( + E_FOCUS_TRACKER (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +focus_tracker_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_FOCUS: + g_value_set_object ( + value, + e_focus_tracker_get_focus ( + E_FOCUS_TRACKER (object))); + return; + + case PROP_WINDOW: + g_value_set_object ( + value, + e_focus_tracker_get_window ( + E_FOCUS_TRACKER (object))); + return; + + case PROP_CUT_CLIPBOARD_ACTION: + g_value_set_object ( + value, + e_focus_tracker_get_cut_clipboard_action ( + E_FOCUS_TRACKER (object))); + return; + + case PROP_COPY_CLIPBOARD_ACTION: + g_value_set_object ( + value, + e_focus_tracker_get_copy_clipboard_action ( + E_FOCUS_TRACKER (object))); + return; + + case PROP_PASTE_CLIPBOARD_ACTION: + g_value_set_object ( + value, + e_focus_tracker_get_paste_clipboard_action ( + E_FOCUS_TRACKER (object))); + return; + + case PROP_DELETE_SELECTION_ACTION: + g_value_set_object ( + value, + e_focus_tracker_get_delete_selection_action ( + E_FOCUS_TRACKER (object))); + return; + + case PROP_SELECT_ALL_ACTION: + g_value_set_object ( + value, + e_focus_tracker_get_select_all_action ( + E_FOCUS_TRACKER (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +focus_tracker_dispose (GObject *object) +{ + EFocusTrackerPrivate *priv; + + priv = E_FOCUS_TRACKER_GET_PRIVATE (object); + + g_signal_handlers_disconnect_matched ( + gtk_clipboard_get (GDK_SELECTION_PRIMARY), + G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, object); + + g_signal_handlers_disconnect_matched ( + gtk_clipboard_get (GDK_SELECTION_CLIPBOARD), + G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, object); + + if (priv->window != NULL) { + g_signal_handlers_disconnect_matched ( + priv->window, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->window); + priv->window = NULL; + } + + if (priv->cut_clipboard != NULL) { + g_signal_handlers_disconnect_matched ( + priv->cut_clipboard, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->cut_clipboard); + priv->cut_clipboard = NULL; + } + + if (priv->copy_clipboard != NULL) { + g_signal_handlers_disconnect_matched ( + priv->copy_clipboard, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->copy_clipboard); + priv->copy_clipboard = NULL; + } + + if (priv->paste_clipboard != NULL) { + g_signal_handlers_disconnect_matched ( + priv->paste_clipboard, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->paste_clipboard); + priv->paste_clipboard = NULL; + } + + if (priv->delete_selection != NULL) { + g_signal_handlers_disconnect_matched ( + priv->delete_selection, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->delete_selection); + priv->delete_selection = NULL; + } + + if (priv->select_all != NULL) { + g_signal_handlers_disconnect_matched ( + priv->select_all, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->select_all); + priv->select_all = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_focus_tracker_parent_class)->dispose (object); +} + +static void +focus_tracker_constructed (GObject *object) +{ + GtkClipboard *clipboard; + + /* Listen for "owner-change" signals from the primary selection + * clipboard to learn when text selections change in GtkEditable + * widgets. It's a bit of an overkill, but I don't know of any + * other notification mechanism. */ + + clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); + + g_signal_connect_swapped ( + clipboard, "owner-change", + G_CALLBACK (e_focus_tracker_update_actions), object); + + /* Listen for "owner-change" signals from the default clipboard + * so we can update the paste action when the user cuts or copies + * something. This is how GEdit does it. */ + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + + g_signal_connect_swapped ( + clipboard, "owner-change", + G_CALLBACK (e_focus_tracker_update_actions), object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_focus_tracker_parent_class)->constructed (object); +} + +static void +e_focus_tracker_class_init (EFocusTrackerClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EFocusTrackerPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = focus_tracker_set_property; + object_class->get_property = focus_tracker_get_property; + object_class->dispose = focus_tracker_dispose; + object_class->constructed = focus_tracker_constructed; + + g_object_class_install_property ( + object_class, + PROP_FOCUS, + g_param_spec_object ( + "focus", + "Focus", + NULL, + GTK_TYPE_WIDGET, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_WINDOW, + g_param_spec_object ( + "window", + "Window", + NULL, + GTK_TYPE_WINDOW, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); + + g_object_class_install_property ( + object_class, + PROP_CUT_CLIPBOARD_ACTION, + g_param_spec_object ( + "cut-clipboard-action", + "Cut Clipboard Action", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_COPY_CLIPBOARD_ACTION, + g_param_spec_object ( + "copy-clipboard-action", + "Copy Clipboard Action", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_PASTE_CLIPBOARD_ACTION, + g_param_spec_object ( + "paste-clipboard-action", + "Paste Clipboard Action", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_DELETE_SELECTION_ACTION, + g_param_spec_object ( + "delete-selection-action", + "Delete Selection Action", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SELECT_ALL_ACTION, + g_param_spec_object ( + "select-all-action", + "Select All Action", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); +} + +static void +e_focus_tracker_init (EFocusTracker *focus_tracker) +{ + GtkAction *action; + + focus_tracker->priv = E_FOCUS_TRACKER_GET_PRIVATE (focus_tracker); + + /* Define dummy actions. These will most likely be overridden, + * but for cases where they're not it ensures ESelectable objects + * will always get a valid GtkAction when they ask us for one. */ + + action = gtk_action_new ( + "cut-clipboard", NULL, + _("Cut the selection"), GTK_STOCK_CUT); + focus_tracker->priv->cut_clipboard = action; + + action = gtk_action_new ( + "copy-clipboard", NULL, + _("Copy the selection"), GTK_STOCK_COPY); + focus_tracker->priv->copy_clipboard = action; + + action = gtk_action_new ( + "paste-clipboard", NULL, + _("Paste the clipboard"), GTK_STOCK_PASTE); + focus_tracker->priv->paste_clipboard = action; + + action = gtk_action_new ( + "delete-selection", NULL, + _("Delete the selection"), GTK_STOCK_DELETE); + focus_tracker->priv->delete_selection = action; + + action = gtk_action_new ( + "select-all", NULL, + _("Select all text"), GTK_STOCK_SELECT_ALL); + focus_tracker->priv->select_all = action; +} + +EFocusTracker * +e_focus_tracker_new (GtkWindow *window) +{ + g_return_val_if_fail (GTK_IS_WINDOW (window), NULL); + + return g_object_new (E_TYPE_FOCUS_TRACKER, "window", window, NULL); +} + +GtkWidget * +e_focus_tracker_get_focus (EFocusTracker *focus_tracker) +{ + g_return_val_if_fail (E_IS_FOCUS_TRACKER (focus_tracker), NULL); + + return focus_tracker->priv->focus; +} + +GtkWindow * +e_focus_tracker_get_window (EFocusTracker *focus_tracker) +{ + g_return_val_if_fail (E_IS_FOCUS_TRACKER (focus_tracker), NULL); + + return focus_tracker->priv->window; +} + +GtkAction * +e_focus_tracker_get_cut_clipboard_action (EFocusTracker *focus_tracker) +{ + g_return_val_if_fail (E_IS_FOCUS_TRACKER (focus_tracker), NULL); + + return focus_tracker->priv->cut_clipboard; +} + +void +e_focus_tracker_set_cut_clipboard_action (EFocusTracker *focus_tracker, + GtkAction *cut_clipboard) +{ + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + if (cut_clipboard != NULL) { + g_return_if_fail (GTK_IS_ACTION (cut_clipboard)); + g_object_ref (cut_clipboard); + } + + if (focus_tracker->priv->cut_clipboard != NULL) { + g_signal_handlers_disconnect_matched ( + focus_tracker->priv->cut_clipboard, + G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, + focus_tracker); + g_object_unref (focus_tracker->priv->cut_clipboard); + } + + focus_tracker->priv->cut_clipboard = cut_clipboard; + + if (cut_clipboard != NULL) + g_signal_connect_swapped ( + cut_clipboard, "activate", + G_CALLBACK (e_focus_tracker_cut_clipboard), + focus_tracker); + + g_object_notify (G_OBJECT (focus_tracker), "cut-clipboard-action"); +} + +GtkAction * +e_focus_tracker_get_copy_clipboard_action (EFocusTracker *focus_tracker) +{ + g_return_val_if_fail (E_IS_FOCUS_TRACKER (focus_tracker), NULL); + + return focus_tracker->priv->copy_clipboard; +} + +void +e_focus_tracker_set_copy_clipboard_action (EFocusTracker *focus_tracker, + GtkAction *copy_clipboard) +{ + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + if (copy_clipboard != NULL) { + g_return_if_fail (GTK_IS_ACTION (copy_clipboard)); + g_object_ref (copy_clipboard); + } + + if (focus_tracker->priv->copy_clipboard != NULL) { + g_signal_handlers_disconnect_matched ( + focus_tracker->priv->copy_clipboard, + G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, + focus_tracker); + g_object_unref (focus_tracker->priv->copy_clipboard); + } + + focus_tracker->priv->copy_clipboard = copy_clipboard; + + if (copy_clipboard != NULL) + g_signal_connect_swapped ( + copy_clipboard, "activate", + G_CALLBACK (e_focus_tracker_copy_clipboard), + focus_tracker); + + g_object_notify (G_OBJECT (focus_tracker), "copy-clipboard-action"); +} + +GtkAction * +e_focus_tracker_get_paste_clipboard_action (EFocusTracker *focus_tracker) +{ + g_return_val_if_fail (E_IS_FOCUS_TRACKER (focus_tracker), NULL); + + return focus_tracker->priv->paste_clipboard; +} + +void +e_focus_tracker_set_paste_clipboard_action (EFocusTracker *focus_tracker, + GtkAction *paste_clipboard) +{ + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + if (paste_clipboard != NULL) { + g_return_if_fail (GTK_IS_ACTION (paste_clipboard)); + g_object_ref (paste_clipboard); + } + + if (focus_tracker->priv->paste_clipboard != NULL) { + g_signal_handlers_disconnect_matched ( + focus_tracker->priv->paste_clipboard, + G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, + focus_tracker); + g_object_unref (focus_tracker->priv->paste_clipboard); + } + + focus_tracker->priv->paste_clipboard = paste_clipboard; + + if (paste_clipboard != NULL) + g_signal_connect_swapped ( + paste_clipboard, "activate", + G_CALLBACK (e_focus_tracker_paste_clipboard), + focus_tracker); + + g_object_notify (G_OBJECT (focus_tracker), "paste-clipboard-action"); +} + +GtkAction * +e_focus_tracker_get_delete_selection_action (EFocusTracker *focus_tracker) +{ + g_return_val_if_fail (E_IS_FOCUS_TRACKER (focus_tracker), NULL); + + return focus_tracker->priv->delete_selection; +} + +void +e_focus_tracker_set_delete_selection_action (EFocusTracker *focus_tracker, + GtkAction *delete_selection) +{ + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + if (delete_selection != NULL) { + g_return_if_fail (GTK_IS_ACTION (delete_selection)); + g_object_ref (delete_selection); + } + + if (focus_tracker->priv->delete_selection != NULL) { + g_signal_handlers_disconnect_matched ( + focus_tracker->priv->delete_selection, + G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, + focus_tracker); + g_object_unref (focus_tracker->priv->delete_selection); + } + + focus_tracker->priv->delete_selection = delete_selection; + + if (delete_selection != NULL) + g_signal_connect_swapped ( + delete_selection, "activate", + G_CALLBACK (e_focus_tracker_delete_selection), + focus_tracker); + + g_object_notify (G_OBJECT (focus_tracker), "delete-selection-action"); +} + +GtkAction * +e_focus_tracker_get_select_all_action (EFocusTracker *focus_tracker) +{ + g_return_val_if_fail (E_IS_FOCUS_TRACKER (focus_tracker), NULL); + + return focus_tracker->priv->select_all; +} + +void +e_focus_tracker_set_select_all_action (EFocusTracker *focus_tracker, + GtkAction *select_all) +{ + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + if (select_all != NULL) { + g_return_if_fail (GTK_IS_ACTION (select_all)); + g_object_ref (select_all); + } + + if (focus_tracker->priv->select_all != NULL) { + g_signal_handlers_disconnect_matched ( + focus_tracker->priv->select_all, + G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, + focus_tracker); + g_object_unref (focus_tracker->priv->select_all); + } + + focus_tracker->priv->select_all = select_all; + + if (select_all != NULL) + g_signal_connect_swapped ( + select_all, "activate", + G_CALLBACK (e_focus_tracker_select_all), + focus_tracker); + + g_object_notify (G_OBJECT (focus_tracker), "select-all-action"); +} + +void +e_focus_tracker_update_actions (EFocusTracker *focus_tracker) +{ + GtkClipboard *clipboard; + + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + /* Request clipboard targets asynchronously. */ + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + + gtk_clipboard_request_targets ( + clipboard, (GtkClipboardTargetsReceivedFunc) + focus_tracker_targets_received_cb, + g_object_ref (focus_tracker)); +} + +void +e_focus_tracker_cut_clipboard (EFocusTracker *focus_tracker) +{ + GtkWidget *focus; + + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + focus = e_focus_tracker_get_focus (focus_tracker); + + if (GTK_IS_EDITABLE (focus)) + gtk_editable_cut_clipboard (GTK_EDITABLE (focus)); + + else if (E_IS_SELECTABLE (focus)) + e_selectable_cut_clipboard (E_SELECTABLE (focus)); +} + +void +e_focus_tracker_copy_clipboard (EFocusTracker *focus_tracker) +{ + GtkWidget *focus; + + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + focus = e_focus_tracker_get_focus (focus_tracker); + + if (GTK_IS_EDITABLE (focus)) + gtk_editable_copy_clipboard (GTK_EDITABLE (focus)); + + else if (E_IS_SELECTABLE (focus)) + e_selectable_copy_clipboard (E_SELECTABLE (focus)); +} + +void +e_focus_tracker_paste_clipboard (EFocusTracker *focus_tracker) +{ + GtkWidget *focus; + + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + focus = e_focus_tracker_get_focus (focus_tracker); + + if (GTK_IS_EDITABLE (focus)) + gtk_editable_paste_clipboard (GTK_EDITABLE (focus)); + + else if (E_IS_SELECTABLE (focus)) + e_selectable_paste_clipboard (E_SELECTABLE (focus)); +} + +void +e_focus_tracker_delete_selection (EFocusTracker *focus_tracker) +{ + GtkWidget *focus; + + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + focus = e_focus_tracker_get_focus (focus_tracker); + + if (GTK_IS_EDITABLE (focus)) + gtk_editable_delete_selection (GTK_EDITABLE (focus)); + + else if (E_IS_SELECTABLE (focus)) + e_selectable_delete_selection (E_SELECTABLE (focus)); +} + +void +e_focus_tracker_select_all (EFocusTracker *focus_tracker) +{ + GtkWidget *focus; + + g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker)); + + focus = e_focus_tracker_get_focus (focus_tracker); + + if (GTK_IS_EDITABLE (focus)) + gtk_editable_select_region (GTK_EDITABLE (focus), 0, -1); + + else if (E_IS_SELECTABLE (focus)) + e_selectable_select_all (E_SELECTABLE (focus)); +} diff --git a/e-util/e-focus-tracker.h b/e-util/e-focus-tracker.h new file mode 100644 index 0000000000..e633d0f57c --- /dev/null +++ b/e-util/e-focus-tracker.h @@ -0,0 +1,104 @@ +/* + * e-focus-tracker.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_FOCUS_TRACKER_H +#define E_FOCUS_TRACKER_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_FOCUS_TRACKER \ + (e_focus_tracker_get_type ()) +#define E_FOCUS_TRACKER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_FOCUS_TRACKER, EFocusTracker)) +#define E_FOCUS_TRACKER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_FOCUS_TRACKER, EFocusTrackerClass)) +#define E_IS_FOCUS_TRACKER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_FOCUS_TRACKER)) +#define E_IS_FOCUS_TRACKER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_FOCUS_TRACKER)) +#define E_FOCUS_TRACKER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_FOCUS_TRACKER, EFocusTrackerClass)) + +G_BEGIN_DECLS + +typedef struct _EFocusTracker EFocusTracker; +typedef struct _EFocusTrackerClass EFocusTrackerClass; +typedef struct _EFocusTrackerPrivate EFocusTrackerPrivate; + +struct _EFocusTracker { + GObject parent; + EFocusTrackerPrivate *priv; +}; + +struct _EFocusTrackerClass { + GObjectClass parent_class; +}; + +GType e_focus_tracker_get_type (void); +EFocusTracker * e_focus_tracker_new (GtkWindow *window); +GtkWidget * e_focus_tracker_get_focus (EFocusTracker *focus_tracker); +GtkWindow * e_focus_tracker_get_window (EFocusTracker *focus_tracker); +GtkAction * e_focus_tracker_get_cut_clipboard_action + (EFocusTracker *focus_tracker); +void e_focus_tracker_set_cut_clipboard_action + (EFocusTracker *focus_tracker, + GtkAction *cut_clipboard); +GtkAction * e_focus_tracker_get_copy_clipboard_action + (EFocusTracker *focus_tracker); +void e_focus_tracker_set_copy_clipboard_action + (EFocusTracker *focus_tracker, + GtkAction *copy_clipboard); +GtkAction * e_focus_tracker_get_paste_clipboard_action + (EFocusTracker *focus_tracker); +void e_focus_tracker_set_paste_clipboard_action + (EFocusTracker *focus_tracker, + GtkAction *paste_clipboard); +GtkAction * e_focus_tracker_get_delete_selection_action + (EFocusTracker *focus_tracker); +void e_focus_tracker_set_delete_selection_action + (EFocusTracker *focus_tracker, + GtkAction *delete_selection); +GtkAction * e_focus_tracker_get_select_all_action + (EFocusTracker *focus_tracker); +void e_focus_tracker_set_select_all_action + (EFocusTracker *focus_tracker, + GtkAction *select_all); +void e_focus_tracker_update_actions (EFocusTracker *focus_tracker); +void e_focus_tracker_cut_clipboard (EFocusTracker *focus_tracker); +void e_focus_tracker_copy_clipboard (EFocusTracker *focus_tracker); +void e_focus_tracker_paste_clipboard (EFocusTracker *focus_tracker); +void e_focus_tracker_delete_selection + (EFocusTracker *focus_tracker); +void e_focus_tracker_select_all (EFocusTracker *focus_tracker); + +G_END_DECLS + +#endif /* E_FOCUS_TRACKER_H */ diff --git a/e-util/e-html-utils.h b/e-util/e-html-utils.h index f87e82f9b1..2fe67fffd5 100644 --- a/e-util/e-html-utils.h +++ b/e-util/e-html-utils.h @@ -20,6 +20,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef __E_HTML_UTILS__ #define __E_HTML_UTILS__ diff --git a/e-util/e-icon-factory.h b/e-util/e-icon-factory.h index 89a7d5a6a4..c75c72fec7 100644 --- a/e-util/e-icon-factory.h +++ b/e-util/e-icon-factory.h @@ -21,6 +21,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef _E_ICON_FACTORY_H_ #define _E_ICON_FACTORY_H_ diff --git a/e-util/e-image-chooser.c b/e-util/e-image-chooser.c new file mode 100644 index 0000000000..20c2f0e473 --- /dev/null +++ b/e-util/e-image-chooser.c @@ -0,0 +1,562 @@ +/* + * e-image-chooser.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <string.h> + +#include <glib/gi18n.h> + +#include "e-image-chooser.h" + +#include "e-icon-factory.h" + +#define E_IMAGE_CHOOSER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_IMAGE_CHOOSER, EImageChooserPrivate)) + +struct _EImageChooserPrivate { + GtkWidget *frame; + GtkWidget *image; + + gchar *image_buf; + gint image_buf_size; + gint image_width; + gint image_height; + + /* Default Image */ + gchar *icon_name; +}; + +enum { + PROP_0, + PROP_ICON_NAME +}; + +enum { + CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +#define URI_LIST_TYPE "text/uri-list" + +G_DEFINE_TYPE ( + EImageChooser, + e_image_chooser, + GTK_TYPE_VBOX) + +static gboolean +set_image_from_data (EImageChooser *chooser, + gchar *data, + gint length) +{ + GdkPixbufLoader *loader; + GdkPixbuf *pixbuf; + gfloat scale; + gint new_height; + gint new_width; + + loader = gdk_pixbuf_loader_new (); + gdk_pixbuf_loader_write (loader, (guchar *) data, length, NULL); + gdk_pixbuf_loader_close (loader, NULL); + + pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); + if (pixbuf) + g_object_ref (pixbuf); + + g_object_unref (loader); + + if (pixbuf == NULL) + return FALSE; + + new_height = gdk_pixbuf_get_height (pixbuf); + new_width = gdk_pixbuf_get_width (pixbuf); + + if (chooser->priv->image_height == 0 + && chooser->priv->image_width == 0) { + scale = 1.0; + } else if (chooser->priv->image_height < new_height + || chooser->priv->image_width < new_width) { + /* we need to scale down */ + if (new_height > new_width) + scale = (gfloat) chooser->priv->image_height / new_height; + else + scale = (gfloat) chooser->priv->image_width / new_width; + } else { + /* we need to scale up */ + if (new_height > new_width) + scale = (gfloat) new_height / chooser->priv->image_height; + else + scale = (gfloat) new_width / chooser->priv->image_width; + } + + if (scale == 1.0) { + gtk_image_set_from_pixbuf ( + GTK_IMAGE (chooser->priv->image), pixbuf); + chooser->priv->image_width = new_width; + chooser->priv->image_height = new_height; + } else { + GdkPixbuf *scaled; + GdkPixbuf *composite; + + new_width *= scale; + new_height *= scale; + new_width = MIN (new_width, chooser->priv->image_width); + new_height = MIN (new_height, chooser->priv->image_height); + + scaled = gdk_pixbuf_scale_simple ( + pixbuf, new_width, new_height, + GDK_INTERP_BILINEAR); + + composite = gdk_pixbuf_new ( + GDK_COLORSPACE_RGB, TRUE, + gdk_pixbuf_get_bits_per_sample (pixbuf), + chooser->priv->image_width, + chooser->priv->image_height); + + gdk_pixbuf_fill (composite, 0x00000000); + + gdk_pixbuf_copy_area ( + scaled, 0, 0, new_width, new_height, + composite, + chooser->priv->image_width / 2 - new_width / 2, + chooser->priv->image_height / 2 - new_height / 2); + + gtk_image_set_from_pixbuf ( + GTK_IMAGE (chooser->priv->image), composite); + + g_object_unref (scaled); + g_object_unref (composite); + } + + g_object_unref (pixbuf); + + g_free (chooser->priv->image_buf); + chooser->priv->image_buf = data; + chooser->priv->image_buf_size = length; + + g_signal_emit (chooser, signals[CHANGED], 0); + + return TRUE; +} + +static gboolean +image_drag_motion_cb (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + EImageChooser *chooser) +{ + GtkFrame *frame; + GList *targets, *p; + + frame = GTK_FRAME (chooser->priv->frame); + targets = gdk_drag_context_list_targets (context); + + for (p = targets; p != NULL; p = p->next) { + gchar *possible_type; + + possible_type = gdk_atom_name (GDK_POINTER_TO_ATOM (p->data)); + if (!strcmp (possible_type, URI_LIST_TYPE)) { + g_free (possible_type); + gdk_drag_status (context, GDK_ACTION_COPY, time); + gtk_frame_set_shadow_type (frame, GTK_SHADOW_IN); + return TRUE; + } + + g_free (possible_type); + } + + gtk_frame_set_shadow_type (frame, GTK_SHADOW_NONE); + + return FALSE; +} + +static void +image_drag_leave_cb (GtkWidget *widget, + GdkDragContext *context, + guint time, + EImageChooser *chooser) +{ + GtkFrame *frame; + + frame = GTK_FRAME (chooser->priv->frame); + gtk_frame_set_shadow_type (frame, GTK_SHADOW_NONE); +} + +static gboolean +image_drag_drop_cb (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + EImageChooser *chooser) +{ + GtkFrame *frame; + GList *targets, *p; + + frame = GTK_FRAME (chooser->priv->frame); + targets = gdk_drag_context_list_targets (context); + + if (targets == NULL) { + gtk_frame_set_shadow_type (frame, GTK_SHADOW_NONE); + return FALSE; + } + + for (p = targets; p != NULL; p = p->next) { + gchar *possible_type; + + possible_type = gdk_atom_name (GDK_POINTER_TO_ATOM (p->data)); + if (!strcmp (possible_type, URI_LIST_TYPE)) { + g_free (possible_type); + gtk_drag_get_data ( + widget, context, + GDK_POINTER_TO_ATOM (p->data), time); + gtk_frame_set_shadow_type (frame, GTK_SHADOW_NONE); + return TRUE; + } + + g_free (possible_type); + } + + gtk_frame_set_shadow_type (frame, GTK_SHADOW_NONE); + + return FALSE; +} + +static void +image_chooser_file_loaded_cb (GFile *file, + GAsyncResult *result, + EImageChooser *chooser) +{ + gchar *contents; + gsize length; + GError *error = NULL; + + g_file_load_contents_finish ( + file, result, &contents, &length, NULL, &error); + + if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + goto exit; + } + + set_image_from_data (chooser, contents, length); + + g_free (contents); + +exit: + g_object_unref (chooser); +} + +static void +image_drag_data_received_cb (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time, + EImageChooser *chooser) +{ + GFile *file; + gboolean handled = FALSE; + gchar **uris; + + uris = gtk_selection_data_get_uris (selection_data); + + if (uris == NULL) + goto exit; + + file = g_file_new_for_uri (uris[0]); + + /* XXX Not cancellable. */ + g_file_load_contents_async ( + file, NULL, (GAsyncReadyCallback) + image_chooser_file_loaded_cb, + g_object_ref (chooser)); + + g_object_unref (file); + g_strfreev (uris); + + /* Assume success. We won't know til later. */ + handled = TRUE; + +exit: + gtk_drag_finish (context, handled, FALSE, time); +} + +static void +image_chooser_set_icon_name (EImageChooser *chooser, + const gchar *icon_name) +{ + GtkIconTheme *icon_theme; + GtkIconInfo *icon_info; + const gchar *filename; + gint width, height; + + g_return_if_fail (chooser->priv->icon_name == NULL); + + chooser->priv->icon_name = g_strdup (icon_name); + + icon_theme = gtk_icon_theme_get_default (); + gtk_icon_size_lookup (GTK_ICON_SIZE_DIALOG, &width, &height); + + icon_info = gtk_icon_theme_lookup_icon ( + icon_theme, icon_name, height, 0); + g_return_if_fail (icon_info != NULL); + + filename = gtk_icon_info_get_filename (icon_info); + e_image_chooser_set_from_file (chooser, filename); + gtk_icon_info_free (icon_info); +} + +static void +image_chooser_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ICON_NAME: + image_chooser_set_icon_name ( + E_IMAGE_CHOOSER (object), + g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +image_chooser_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ICON_NAME: + g_value_set_string ( + value, + e_image_chooser_get_icon_name ( + E_IMAGE_CHOOSER (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +image_chooser_dispose (GObject *object) +{ + EImageChooserPrivate *priv; + + priv = E_IMAGE_CHOOSER_GET_PRIVATE (object); + + if (priv->frame != NULL) { + g_object_unref (priv->frame); + priv->frame = NULL; + } + + if (priv->image != NULL) { + g_object_unref (priv->image); + priv->image = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_image_chooser_parent_class)->dispose (object); +} + +static void +image_chooser_finalize (GObject *object) +{ + EImageChooserPrivate *priv; + + priv = E_IMAGE_CHOOSER_GET_PRIVATE (object); + + g_free (priv->image_buf); + g_free (priv->icon_name); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_image_chooser_parent_class)->finalize (object); +} + +static void +e_image_chooser_class_init (EImageChooserClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EImageChooserPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = image_chooser_set_property; + object_class->get_property = image_chooser_get_property; + object_class->dispose = image_chooser_dispose; + object_class->finalize = image_chooser_finalize; + + g_object_class_install_property ( + object_class, + PROP_ICON_NAME, + g_param_spec_string ( + "icon-name", + "Icon Name", + NULL, + "avatar-default", + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); + + signals[CHANGED] = g_signal_new ( + "changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (EImageChooserClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_image_chooser_init (EImageChooser *chooser) +{ + GtkWidget *container; + GtkWidget *widget; + + chooser->priv = E_IMAGE_CHOOSER_GET_PRIVATE (chooser); + + container = GTK_WIDGET (chooser); + + widget = gtk_frame_new (""); + gtk_frame_set_shadow_type (GTK_FRAME (widget), GTK_SHADOW_NONE); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + chooser->priv->frame = g_object_ref (widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_alignment_new (0, 0, 0, 0); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_image_new (); + gtk_container_add (GTK_CONTAINER (container), widget); + chooser->priv->image = g_object_ref (widget); + gtk_widget_show (widget); + + gtk_drag_dest_set (widget, 0, NULL, 0, GDK_ACTION_COPY); + gtk_drag_dest_add_uri_targets (widget); + + g_signal_connect ( + widget, "drag-motion", + G_CALLBACK (image_drag_motion_cb), chooser); + g_signal_connect ( + widget, "drag-leave", + G_CALLBACK (image_drag_leave_cb), chooser); + g_signal_connect ( + widget, "drag-drop", + G_CALLBACK (image_drag_drop_cb), chooser); + g_signal_connect ( + widget, "drag-data-received", + G_CALLBACK (image_drag_data_received_cb), chooser); +} + +const gchar * +e_image_chooser_get_icon_name (EImageChooser *chooser) +{ + g_return_val_if_fail (E_IS_IMAGE_CHOOSER (chooser), NULL); + + return chooser->priv->icon_name; +} + +GtkWidget * +e_image_chooser_new (const gchar *icon_name) +{ + g_return_val_if_fail (icon_name != NULL, NULL); + + return g_object_new ( + E_TYPE_IMAGE_CHOOSER, + "icon-name", icon_name, NULL); +} + +gboolean +e_image_chooser_set_from_file (EImageChooser *chooser, + const gchar *filename) +{ + gchar *data; + gsize data_length; + + g_return_val_if_fail (E_IS_IMAGE_CHOOSER (chooser), FALSE); + g_return_val_if_fail (filename != NULL, FALSE); + + if (!g_file_get_contents (filename, &data, &data_length, NULL)) + return FALSE; + + if (!set_image_from_data (chooser, data, data_length)) + g_free (data); + + return TRUE; +} + +gboolean +e_image_chooser_get_image_data (EImageChooser *chooser, + gchar **data, + gsize *data_length) +{ + g_return_val_if_fail (E_IS_IMAGE_CHOOSER (chooser), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (data_length != NULL, FALSE); + + *data_length = chooser->priv->image_buf_size; + *data = g_malloc (*data_length); + memcpy (*data, chooser->priv->image_buf, *data_length); + + return TRUE; +} + +gboolean +e_image_chooser_set_image_data (EImageChooser *chooser, + gchar *data, + gsize data_length) +{ + gchar *buf; + + g_return_val_if_fail (E_IS_IMAGE_CHOOSER (chooser), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + /* yuck, a copy... */ + buf = g_malloc (data_length); + memcpy (buf, data, data_length); + + if (!set_image_from_data (chooser, buf, data_length)) { + g_free (buf); + return FALSE; + } + + return TRUE; +} diff --git a/e-util/e-image-chooser.h b/e-util/e-image-chooser.h new file mode 100644 index 0000000000..d9bfb34b71 --- /dev/null +++ b/e-util/e-image-chooser.h @@ -0,0 +1,80 @@ +/* + * e-image-chooser.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_IMAGE_CHOOSER_H +#define E_IMAGE_CHOOSER_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_IMAGE_CHOOSER \ + (e_image_chooser_get_type ()) +#define E_IMAGE_CHOOSER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_IMAGE_CHOOSER, EImageChooser)) +#define E_IMAGE_CHOOSER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_IMAGE_CHOOSER, EImageChooserClass)) +#define E_IS_IMAGE_CHOOSER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_IMAGE_CHOOSER)) +#define E_IS_IMAGE_CHOOSER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_IMAGE_CHOOSER)) +#define E_IMAGE_CHOOSER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_IMAGE_CHOOSER, EImageChooserClass)) + +G_BEGIN_DECLS + +typedef struct _EImageChooser EImageChooser; +typedef struct _EImageChooserClass EImageChooserClass; +typedef struct _EImageChooserPrivate EImageChooserPrivate; + +struct _EImageChooser { + GtkBox parent; + EImageChooserPrivate *priv; +}; + +struct _EImageChooserClass { + GtkBoxClass parent_class; + + /* signals */ + void (*changed) (EImageChooser *chooser); +}; + +GType e_image_chooser_get_type (void); +GtkWidget * e_image_chooser_new (const gchar *icon_name); +const gchar * e_image_chooser_get_icon_name (EImageChooser *chooser); +gboolean e_image_chooser_set_from_file (EImageChooser *chooser, + const gchar *filename); +gboolean e_image_chooser_set_image_data (EImageChooser *chooser, + gchar *data, + gsize data_length); +gboolean e_image_chooser_get_image_data (EImageChooser *chooser, + gchar **data, + gsize *data_length); + +#endif /* E_IMAGE_CHOOSER_H */ diff --git a/e-util/e-import-assistant.c b/e-util/e-import-assistant.c new file mode 100644 index 0000000000..ae48e5c217 --- /dev/null +++ b/e-util/e-import-assistant.c @@ -0,0 +1,1436 @@ +/* + * e-import-assistant.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-import-assistant.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <string.h> + +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> +#include <libebackend/libebackend.h> + +#include "e-import.h" +#include "e-util-private.h" + +#define E_IMPORT_ASSISTANT_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_IMPORT_ASSISTANT, EImportAssistantPrivate)) + +typedef struct _ImportFilePage ImportFilePage; +typedef struct _ImportDestinationPage ImportDestinationPage; +typedef struct _ImportTypePage ImportTypePage; +typedef struct _ImportSelectionPage ImportSelectionPage; +typedef struct _ImportProgressPage ImportProgressPage; +typedef struct _ImportSimplePage ImportSimplePage; + +struct _ImportFilePage { + GtkWidget *filename; + GtkWidget *filetype; + + EImportTargetURI *target; + EImportImporter *importer; +}; + +struct _ImportDestinationPage { + GtkWidget *control; +}; + +struct _ImportTypePage { + GtkWidget *intelligent; + GtkWidget *file; +}; + +struct _ImportSelectionPage { + GSList *importers; + GSList *current; + EImportTargetHome *target; +}; + +struct _ImportProgressPage { + GtkWidget *progress_bar; +}; + +struct _ImportSimplePage { + GtkWidget *actionlabel; + GtkWidget *filetypetable; + GtkWidget *filetype; + GtkWidget *control; /* importer's destination or preview widget in an alignment */ + gboolean has_preview; /* TRUE when 'control' holds a preview widget, + otherwise holds destination widget */ + + EImportTargetURI *target; + EImportImporter *importer; +}; + +struct _EImportAssistantPrivate { + ImportFilePage file_page; + ImportDestinationPage destination_page; + ImportTypePage type_page; + ImportSelectionPage selection_page; + ImportProgressPage progress_page; + ImportSimplePage simple_page; + + EImport *import; + + gboolean is_simple; + GPtrArray *fileuris; /* each element is a file URI, as a newly allocated string */ + + /* Used for importing phase of operation */ + EImportTarget *import_target; + EImportImporter *import_importer; +}; + +enum { + PAGE_START, + PAGE_INTELI_OR_DIRECT, + PAGE_INTELI_SOURCE, + PAGE_FILE_CHOOSE, + PAGE_FILE_DEST, + PAGE_FINISH, + PAGE_PROGRESS +}; + +enum { + PROP_0, + PROP_IS_SIMPLE +}; + +enum { + FINISHED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE_WITH_CODE ( + EImportAssistant, + e_import_assistant, + GTK_TYPE_ASSISTANT, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +/* Importing functions */ + +static void +import_assistant_emit_finished (EImportAssistant *import_assistant) +{ + g_signal_emit (import_assistant, signals[FINISHED], 0); +} + +static void +filename_changed (GtkWidget *widget, + GtkAssistant *assistant) +{ + EImportAssistantPrivate *priv; + ImportFilePage *page; + const gchar *filename; + gint fileok; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + page = &priv->file_page; + + filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (widget)); + + fileok = + filename != NULL && *filename != '\0' && + g_file_test (filename, G_FILE_TEST_IS_REGULAR); + + if (fileok) { + GtkTreeIter iter; + GtkTreeModel *model; + gboolean valid; + GSList *l; + EImportImporter *first = NULL; + gint i = 0, firstitem = 0; + + g_free (page->target->uri_src); + page->target->uri_src = g_filename_to_uri (filename, NULL, NULL); + + l = e_import_get_importers ( + priv->import, (EImportTarget *) page->target); + model = gtk_combo_box_get_model (GTK_COMBO_BOX (page->filetype)); + valid = gtk_tree_model_get_iter_first (model, &iter); + while (valid) { + gpointer eii = NULL; + + gtk_tree_model_get (model, &iter, 2, &eii, -1); + + if (g_slist_find (l, eii) != NULL) { + if (first == NULL) { + firstitem = i; + first = eii; + } + gtk_list_store_set (GTK_LIST_STORE (model), &iter, 1, TRUE, -1); + } else { + if (page->importer == eii) + page->importer = NULL; + gtk_list_store_set (GTK_LIST_STORE (model), &iter, 1, FALSE, -1); + } + i++; + valid = gtk_tree_model_iter_next (model, &iter); + } + g_slist_free (l); + + if (page->importer == NULL && first) { + page->importer = first; + gtk_combo_box_set_active (GTK_COMBO_BOX (page->filetype), firstitem); + } + fileok = first != NULL; + } else { + GtkTreeIter iter; + GtkTreeModel *model; + gboolean valid; + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (page->filetype)); + for (valid = gtk_tree_model_get_iter_first (model, &iter); + valid; + valid = gtk_tree_model_iter_next (model, &iter)) { + gtk_list_store_set (GTK_LIST_STORE (model), &iter, 1, FALSE, -1); + } + } + + widget = gtk_assistant_get_nth_page (assistant, PAGE_FILE_CHOOSE); + gtk_assistant_set_page_complete (assistant, widget, fileok); +} + +static void +filetype_changed_cb (GtkComboBox *combo_box, + GtkAssistant *assistant) +{ + EImportAssistantPrivate *priv; + GtkTreeModel *model; + GtkTreeIter iter; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + + g_return_if_fail (gtk_combo_box_get_active_iter (combo_box, &iter)); + + model = gtk_combo_box_get_model (combo_box); + gtk_tree_model_get (model, &iter, 2, &priv->file_page.importer, -1); + filename_changed (priv->file_page.filename, assistant); +} + +static GtkWidget * +import_assistant_file_page_init (EImportAssistant *import_assistant) +{ + GtkWidget *page; + GtkWidget *label; + GtkWidget *container; + GtkWidget *widget; + GtkCellRenderer *cell; + GtkListStore *store; + const gchar *text; + gint row = 0; + + page = gtk_vbox_new (FALSE, 6); + gtk_container_set_border_width (GTK_CONTAINER (page), 12); + gtk_widget_show (page); + + container = page; + + text = _("Choose the file that you want to import into Evolution, " + "and select what type of file it is from the list."); + + widget = gtk_label_new (text); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, TRUE, 0); + gtk_widget_show (widget); + + widget = gtk_table_new (2, 2, FALSE); + gtk_table_set_row_spacings (GTK_TABLE (widget), 2); + gtk_table_set_col_spacings (GTK_TABLE (widget), 10); + gtk_container_set_border_width (GTK_CONTAINER (widget), 8); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_label_new_with_mnemonic (_("F_ilename:")); + gtk_misc_set_alignment (GTK_MISC (widget), 1, 0.5); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, row, row + 1, GTK_FILL, 0, 0, 0); + gtk_widget_show (widget); + + label = widget; + + widget = gtk_file_chooser_button_new ( + _("Select a file"), GTK_FILE_CHOOSER_ACTION_OPEN); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget); + gtk_table_attach ( + GTK_TABLE (container), widget, 1, 2, + row, row + 1, GTK_EXPAND | GTK_FILL, 0, 0, 0); + import_assistant->priv->file_page.filename = widget; + gtk_widget_show (widget); + + g_signal_connect ( + widget, "selection-changed", + G_CALLBACK (filename_changed), import_assistant); + + row++; + + widget = gtk_label_new_with_mnemonic (_("File _type:")); + gtk_misc_set_alignment (GTK_MISC (widget), 1, 0.5); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, row, row + 1, GTK_FILL, 0, 0, 0); + gtk_widget_show (widget); + + label = widget; + + store = gtk_list_store_new ( + 3, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER); + widget = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget); + gtk_table_attach ( + GTK_TABLE (container), widget, + 1, 2, row, row + 1, GTK_EXPAND | GTK_FILL, 0, 0, 0); + import_assistant->priv->file_page.filetype = widget; + gtk_widget_show (widget); + g_object_unref (store); + + cell = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (widget), cell, TRUE); + gtk_cell_layout_set_attributes ( + GTK_CELL_LAYOUT (widget), cell, + "text", 0, "sensitive", 1, NULL); + + return page; +} + +static GtkWidget * +import_assistant_destination_page_init (EImportAssistant *import_assistant) +{ + GtkWidget *page; + GtkWidget *container; + GtkWidget *widget; + const gchar *text; + + page = gtk_vbox_new (FALSE, 6); + gtk_container_set_border_width (GTK_CONTAINER (page), 12); + gtk_widget_show (page); + + container = page; + + text = _("Choose the destination for this import"); + + widget = gtk_label_new (text); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, TRUE, 0); + gtk_widget_show (widget); + + return page; +} + +static GtkWidget * +import_assistant_type_page_init (EImportAssistant *import_assistant) +{ + GtkRadioButton *radio_button; + GtkWidget *page; + GtkWidget *container; + GtkWidget *widget; + const gchar *text; + + page = gtk_vbox_new (FALSE, 6); + gtk_container_set_border_width (GTK_CONTAINER (page), 12); + gtk_widget_show (page); + + container = page; + + text = _("Choose the type of importer to run:"); + + widget = gtk_label_new (text); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, TRUE, 0); + gtk_widget_show (widget); + + widget = gtk_radio_button_new_with_mnemonic ( + NULL, _("Import data and settings from _older programs")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + import_assistant->priv->type_page.intelligent = widget; + gtk_widget_show (widget); + + radio_button = GTK_RADIO_BUTTON (widget); + + widget = gtk_radio_button_new_with_mnemonic_from_widget ( + radio_button, _("Import a _single file")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + import_assistant->priv->type_page.file = widget; + gtk_widget_show (widget); + + return page; +} + +static GtkWidget * +import_assistant_selection_page_init (EImportAssistant *import_assistant) +{ + GtkWidget *page; + GtkWidget *container; + GtkWidget *widget; + const gchar *text; + + page = gtk_vbox_new (FALSE, 6); + gtk_container_set_border_width (GTK_CONTAINER (page), 12); + gtk_widget_show (page); + + container = page; + + text = _("Please select the information " + "that you would like to import:"); + + widget = gtk_label_new (text); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, TRUE, 0); + gtk_widget_show (widget); + + widget = gtk_hseparator_new (); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + return page; +} + +static GtkWidget * +import_assistant_progress_page_init (EImportAssistant *import_assistant) +{ + GtkWidget *page; + GtkWidget *container; + GtkWidget *widget; + + page = gtk_vbox_new (FALSE, 6); + gtk_container_set_border_width (GTK_CONTAINER (page), 12); + gtk_widget_show (page); + + container = page; + + widget = gtk_progress_bar_new (); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, FALSE, 0); + import_assistant->priv->progress_page.progress_bar = widget; + gtk_widget_show (widget); + + return page; +} + +static GtkWidget * +import_assistant_simple_page_init (EImportAssistant *import_assistant) +{ + GtkWidget *page; + GtkWidget *label; + GtkWidget *container; + GtkWidget *widget; + GtkCellRenderer *cell; + GtkListStore *store; + gint row = 0; + + page = gtk_vbox_new (FALSE, 6); + gtk_container_set_border_width (GTK_CONTAINER (page), 12); + gtk_widget_show (page); + + container = page; + + widget = gtk_label_new (""); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, TRUE, 0); + gtk_widget_show (widget); + import_assistant->priv->simple_page.actionlabel = widget; + + widget = gtk_table_new (2, 1, FALSE); + gtk_table_set_row_spacings (GTK_TABLE (widget), 2); + gtk_table_set_col_spacings (GTK_TABLE (widget), 10); + gtk_container_set_border_width (GTK_CONTAINER (widget), 8); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, TRUE, 0); + gtk_widget_show (widget); + import_assistant->priv->simple_page.filetypetable = widget; + + container = widget; + + widget = gtk_label_new_with_mnemonic (_("File _type:")); + gtk_misc_set_alignment (GTK_MISC (widget), 1, 0.5); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, row, row + 1, GTK_FILL, 0, 0, 0); + gtk_widget_show (widget); + + label = widget; + + store = gtk_list_store_new ( + 3, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER); + widget = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget); + gtk_table_attach ( + GTK_TABLE (container), widget, + 1, 2, row, row + 1, GTK_EXPAND | GTK_FILL, 0, 0, 0); + import_assistant->priv->simple_page.filetype = widget; + gtk_widget_show (widget); + g_object_unref (store); + + cell = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (widget), cell, TRUE); + gtk_cell_layout_set_attributes ( + GTK_CELL_LAYOUT (widget), cell, + "text", 0, "sensitive", 1, NULL); + + import_assistant->priv->simple_page.control = NULL; + + return page; +} + +static void +prepare_intelligent_page (GtkAssistant *assistant, + GtkWidget *vbox) +{ + EImportAssistantPrivate *priv; + GSList *l; + GtkWidget *table; + gint row; + ImportSelectionPage *page; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + page = &priv->selection_page; + + if (page->target != NULL) { + gtk_assistant_set_page_complete (assistant, vbox, FALSE); + return; + } + + page->target = e_import_target_new_home (priv->import); + + if (page->importers) + g_slist_free (page->importers); + l = page->importers = + e_import_get_importers ( + priv->import, (EImportTarget *) page->target); + + if (l == NULL) { + GtkWidget *widget; + const gchar *text; + + text = _("Evolution checked for settings to import from " + "the following applications: Pine, Netscape, Elm, " + "iCalendar. No importable settings found. If you " + "would like to try again, please click the " + "\"Back\" button."); + + widget = gtk_label_new (text); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_box_pack_start (GTK_BOX (vbox), widget, FALSE, TRUE, 0); + gtk_widget_show (widget); + + gtk_assistant_set_page_complete (assistant, vbox, FALSE); + + return; + } + + table = gtk_table_new (g_slist_length (l), 2, FALSE); + row = 0; + for (; l; l = l->next) { + EImportImporter *eii = l->data; + gchar *str; + GtkWidget *w, *label; + + w = e_import_get_widget ( + priv->import, (EImportTarget *) page->target, eii); + + str = g_strdup_printf (_("From %s:"), eii->name); + label = gtk_label_new (str); + gtk_widget_show (label); + g_free (str); + + gtk_misc_set_alignment (GTK_MISC (label), 0, .5); + + gtk_table_attach ( + GTK_TABLE (table), label, + 0, 1, row, row + 1, GTK_FILL, 0, 0, 0); + if (w) + gtk_table_attach ( + GTK_TABLE (table), w, + 1, 2, row, row + 1, GTK_FILL, 0, 3, 0); + row++; + } + + gtk_widget_show (table); + gtk_box_pack_start (GTK_BOX (vbox), table, FALSE, FALSE, 0); + + gtk_assistant_set_page_complete (assistant, vbox, TRUE); +} + +static void +import_status (EImport *import, + const gchar *what, + gint percent, + gpointer user_data) +{ + EImportAssistant *import_assistant = user_data; + GtkProgressBar *progress_bar; + + progress_bar = GTK_PROGRESS_BAR ( + import_assistant->priv->progress_page.progress_bar); + gtk_progress_bar_set_fraction (progress_bar, percent / 100.0); + gtk_progress_bar_set_text (progress_bar, what); +} + +static void +import_done (EImport *ei, + gpointer user_data) +{ + EImportAssistant *import_assistant = user_data; + + import_assistant_emit_finished (import_assistant); +} + +static void +import_simple_done (EImport *ei, + gpointer user_data) +{ + EImportAssistant *import_assistant = user_data; + EImportAssistantPrivate *priv; + + g_return_if_fail (import_assistant != NULL); + + priv = import_assistant->priv; + g_return_if_fail (priv != NULL); + g_return_if_fail (priv->fileuris != NULL); + g_return_if_fail (priv->simple_page.target != NULL); + + if (import_assistant->priv->fileuris->len > 0) { + import_status (ei, "", 0, import_assistant); + + /* process next file URI */ + g_free (priv->simple_page.target->uri_src); + priv->simple_page.target->uri_src = + g_ptr_array_remove_index (priv->fileuris, 0); + + e_import_import ( + priv->import, priv->import_target, + priv->import_importer, import_status, + import_simple_done, import_assistant); + } else + import_done (ei, import_assistant); +} + +static void +import_intelligent_done (EImport *ei, + gpointer user_data) +{ + EImportAssistant *import_assistant = user_data; + ImportSelectionPage *page; + + page = &import_assistant->priv->selection_page; + + if (page->current && (page->current = page->current->next)) { + import_status (ei, "", 0, import_assistant); + import_assistant->priv->import_importer = page->current->data; + e_import_import ( + import_assistant->priv->import, + (EImportTarget *) page->target, + import_assistant->priv->import_importer, + import_status, import_intelligent_done, + import_assistant); + } else + import_done (ei, import_assistant); +} + +static void +import_cancelled (EImportAssistant *assistant) +{ + e_import_cancel ( + assistant->priv->import, + assistant->priv->import_target, + assistant->priv->import_importer); +} + +static void +prepare_file_page (GtkAssistant *assistant, + GtkWidget *vbox) +{ + EImportAssistantPrivate *priv; + GSList *importers, *imp; + GtkListStore *store; + ImportFilePage *page; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + page = &priv->file_page; + + if (page->target != NULL) { + filename_changed (priv->file_page.filename, assistant); + return; + } + + page->target = e_import_target_new_uri (priv->import, NULL, NULL); + importers = e_import_get_importers (priv->import, (EImportTarget *) page->target); + + store = GTK_LIST_STORE (gtk_combo_box_get_model (GTK_COMBO_BOX (page->filetype))); + gtk_list_store_clear (store); + + for (imp = importers; imp; imp = imp->next) { + GtkTreeIter iter; + EImportImporter *eii = imp->data; + + gtk_list_store_append (store, &iter); + gtk_list_store_set ( + store, &iter, + 0, eii->name, + 1, TRUE, + 2, eii, + -1); + } + + g_slist_free (importers); + + gtk_combo_box_set_active (GTK_COMBO_BOX (page->filetype), 0); + + filename_changed (priv->file_page.filename, assistant); + + g_signal_connect ( + page->filetype, "changed", + G_CALLBACK (filetype_changed_cb), assistant); +} + +static GtkWidget * +create_importer_control (EImport *import, + EImportTarget *target, + EImportImporter *importer) +{ + GtkWidget *control; + + control = e_import_get_widget (import, target, importer); + if (control == NULL) { + /* Coding error, not needed for translators */ + control = gtk_label_new ( + "** PLUGIN ERROR ** No settings for importer"); + gtk_widget_show (control); + } + + return control; +} + +static gboolean +prepare_destination_page (GtkAssistant *assistant, + GtkWidget *vbox) +{ + EImportAssistantPrivate *priv; + ImportDestinationPage *page; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + page = &priv->destination_page; + + if (page->control) + gtk_container_remove (GTK_CONTAINER (vbox), page->control); + + page->control = create_importer_control ( + priv->import, (EImportTarget *) + priv->file_page.target, priv->file_page.importer); + + gtk_box_pack_start (GTK_BOX (vbox), page->control, TRUE, TRUE, 0); + gtk_assistant_set_page_complete (assistant, vbox, TRUE); + + return FALSE; +} + +static void +prepare_progress_page (GtkAssistant *assistant, + GtkWidget *vbox) +{ + EImportAssistantPrivate *priv; + EImportCompleteFunc done = NULL; + ImportSelectionPage *page; + GtkWidget *cancel_button; + gboolean intelligent_import; + gboolean is_simple = FALSE; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + page = &priv->selection_page; + + /* Because we're a GTK_ASSISTANT_PAGE_PROGRESS, this will + * prevent the assistant window from being closed via window + * manager decorations while importing. */ + gtk_assistant_commit (assistant); + + /* Install a custom "Cancel Import" button. */ + cancel_button = gtk_button_new_with_mnemonic (_("_Cancel Import")); + gtk_button_set_image ( + GTK_BUTTON (cancel_button), + gtk_image_new_from_stock ( + GTK_STOCK_CANCEL, GTK_ICON_SIZE_BUTTON)); + g_signal_connect_swapped ( + cancel_button, "clicked", + G_CALLBACK (import_cancelled), assistant); + gtk_assistant_add_action_widget (assistant, cancel_button); + gtk_widget_show (cancel_button); + + g_object_get (assistant, "is-simple", &is_simple, NULL); + + intelligent_import = is_simple ? FALSE : gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (priv->type_page.intelligent)); + + if (is_simple) { + priv->import_importer = priv->simple_page.importer; + priv->import_target = (EImportTarget *) priv->simple_page.target; + done = import_simple_done; + } else if (intelligent_import) { + page->current = page->importers; + if (page->current) { + priv->import_target = (EImportTarget *) page->target; + priv->import_importer = page->current->data; + done = import_intelligent_done; + } + } else { + if (priv->file_page.importer) { + priv->import_importer = priv->file_page.importer; + priv->import_target = (EImportTarget *) priv->file_page.target; + done = import_done; + } + } + + if (done) + e_import_import ( + priv->import, priv->import_target, + priv->import_importer, import_status, + done, assistant); + else + import_assistant_emit_finished (E_IMPORT_ASSISTANT (assistant)); +} + +static void +simple_filetype_changed_cb (GtkComboBox *combo_box, + GtkAssistant *assistant) +{ + EImportAssistantPrivate *priv; + ImportSimplePage *page; + GtkTreeModel *model; + GtkTreeIter iter; + GtkWidget *vbox; + GtkWidget *control; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + page = &priv->simple_page; + + g_return_if_fail (gtk_combo_box_get_active_iter (combo_box, &iter)); + + model = gtk_combo_box_get_model (combo_box); + gtk_tree_model_get (model, &iter, 2, &page->importer, -1); + + vbox = g_object_get_data (G_OBJECT (combo_box), "page-vbox"); + g_return_if_fail (vbox != NULL); + + if (page->control) + gtk_widget_destroy (page->control); + page->has_preview = FALSE; + + control = e_import_get_preview_widget ( + priv->import, (EImportTarget *) + page->target, page->importer); + if (control) { + page->has_preview = TRUE; + gtk_widget_set_size_request (control, 440, 360); + } else + control = create_importer_control ( + priv->import, (EImportTarget *) + page->target, page->importer); + + page->control = gtk_alignment_new (0.0, 0.0, 1.0, 1.0); + gtk_widget_show (page->control); + gtk_container_add (GTK_CONTAINER (page->control), control); + + gtk_box_pack_start (GTK_BOX (vbox), page->control, TRUE, TRUE, 0); + gtk_assistant_set_page_complete (assistant, vbox, TRUE); +} + +static void +prepare_simple_page (GtkAssistant *assistant, + GtkWidget *vbox) +{ + EImportAssistantPrivate *priv; + GSList *importers, *imp; + GtkListStore *store; + ImportSimplePage *page; + gchar *uri; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + page = &priv->simple_page; + + g_return_if_fail (priv->fileuris != NULL); + + if (page->target != NULL) { + return; + } + + uri = g_ptr_array_remove_index (priv->fileuris, 0); + page->target = e_import_target_new_uri (priv->import, uri, NULL); + g_free (uri); + importers = e_import_get_importers (priv->import, (EImportTarget *) page->target); + + store = GTK_LIST_STORE (gtk_combo_box_get_model (GTK_COMBO_BOX (page->filetype))); + gtk_list_store_clear (store); + + for (imp = importers; imp; imp = imp->next) { + GtkTreeIter iter; + EImportImporter *eii = imp->data; + + gtk_list_store_append (store, &iter); + gtk_list_store_set ( + store, &iter, + 0, eii->name, + 1, TRUE, + 2, eii, + -1); + } + + gtk_combo_box_set_active (GTK_COMBO_BOX (page->filetype), 0); + g_object_set_data (G_OBJECT (page->filetype), "page-vbox", vbox); + + simple_filetype_changed_cb (GTK_COMBO_BOX (page->filetype), assistant); + + g_signal_connect ( + page->filetype, "changed", + G_CALLBACK (simple_filetype_changed_cb), assistant); + + if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL) == 1) { + gchar *title; + + /* only one importer found, make it even simpler */ + gtk_label_set_text ( + GTK_LABEL (page->actionlabel), + page->has_preview ? + _("Preview data to be imported") : + _("Choose the destination for this import")); + + gtk_widget_hide (page->filetypetable); + + title = g_strconcat ( + _("Import Data"), " - ", + ((EImportImporter *) importers->data)->name, NULL); + gtk_assistant_set_page_title (assistant, vbox, title); + g_free (title); + } else { + /* multiple importers found, be able to choose from them */ + gtk_label_set_text ( + GTK_LABEL (page->actionlabel), + _("Select what type of file you " + "want to import from the list.")); + + gtk_widget_show (page->filetypetable); + + gtk_assistant_set_page_title (assistant, vbox, _("Import Data")); + } + + g_slist_free (importers); +} + +static gboolean +prepare_simple_destination_page (GtkAssistant *assistant, + GtkWidget *vbox) +{ + EImportAssistantPrivate *priv; + ImportDestinationPage *page; + ImportSimplePage *simple_page; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + page = &priv->destination_page; + simple_page = &priv->simple_page; + + if (page->control) + gtk_container_remove (GTK_CONTAINER (vbox), page->control); + + page->control = create_importer_control ( + priv->import, (EImportTarget *) + simple_page->target, simple_page->importer); + + gtk_box_pack_start (GTK_BOX (vbox), page->control, TRUE, TRUE, 0); + gtk_assistant_set_page_complete (assistant, vbox, TRUE); + + return FALSE; +} + +static gint +forward_cb (gint current_page, + EImportAssistant *import_assistant) +{ + GtkToggleButton *toggle_button; + gboolean is_simple = FALSE; + + g_object_get (import_assistant, "is-simple", &is_simple, NULL); + + if (is_simple) { + if (!import_assistant->priv->simple_page.has_preview) + current_page++; + + return current_page + 1; + } + + toggle_button = GTK_TOGGLE_BUTTON ( + import_assistant->priv->type_page.intelligent); + + switch (current_page) { + case PAGE_INTELI_OR_DIRECT: + if (gtk_toggle_button_get_active (toggle_button)) + return PAGE_INTELI_SOURCE; + else + return PAGE_FILE_CHOOSE; + case PAGE_INTELI_SOURCE: + return PAGE_FINISH; + } + + return current_page + 1; +} + +static gboolean +set_import_uris (EImportAssistant *assistant, + const gchar * const *uris) +{ + EImportAssistantPrivate *priv; + GPtrArray *fileuris = NULL; + gint i; + + g_return_val_if_fail (assistant != NULL, FALSE); + g_return_val_if_fail (assistant->priv != NULL, FALSE); + g_return_val_if_fail (assistant->priv->import != NULL, FALSE); + g_return_val_if_fail (uris != NULL, FALSE); + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (assistant); + + for (i = 0; uris[i]; i++) { + const gchar *uri = uris[i]; + gchar *filename; + + filename = g_filename_from_uri (uri, NULL, NULL); + if (!filename) + filename = g_strdup (uri); + + if (filename && *filename && + g_file_test (filename, G_FILE_TEST_IS_REGULAR)) { + gchar *furi; + + if (!g_path_is_absolute (filename)) { + gchar *tmp, *curr; + + curr = g_get_current_dir (); + tmp = g_build_filename (curr, filename, NULL); + g_free (curr); + + g_free (filename); + filename = tmp; + } + + if (fileuris == NULL) { + EImportTargetURI *target; + GSList *importers; + + furi = g_filename_to_uri (filename, NULL, NULL); + target = e_import_target_new_uri (priv->import, furi, NULL); + importers = e_import_get_importers ( + priv->import, (EImportTarget *) target); + + if (importers != NULL) { + /* there is at least one importer which can be used, + * thus there can be done an import */ + fileuris = g_ptr_array_new (); + } + + g_slist_free (importers); + e_import_target_free (priv->import, target); + g_free (furi); + + if (fileuris == NULL) { + g_free (filename); + break; + } + } + + furi = g_filename_to_uri (filename, NULL, NULL); + if (furi) + g_ptr_array_add (fileuris, furi); + } + + g_free (filename); + } + + if (fileuris != NULL) { + priv->fileuris = fileuris; + } + + return fileuris != NULL; +} + +static void +import_assistant_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + EImportAssistantPrivate *priv; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (object); + + switch (property_id) { + case PROP_IS_SIMPLE: + priv->is_simple = g_value_get_boolean (value); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +import_assistant_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EImportAssistantPrivate *priv; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (object); + + switch (property_id) { + case PROP_IS_SIMPLE: + g_value_set_boolean (value, priv->is_simple); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +import_assistant_dispose (GObject *object) +{ + EImportAssistantPrivate *priv; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (object); + + if (priv->file_page.target != NULL) { + e_import_target_free ( + priv->import, (EImportTarget *) + priv->file_page.target); + priv->file_page.target = NULL; + } + + if (priv->selection_page.target != NULL) { + e_import_target_free ( + priv->import, (EImportTarget *) + priv->selection_page.target); + priv->selection_page.target = NULL; + } + + if (priv->simple_page.target != NULL) { + e_import_target_free ( + priv->import, (EImportTarget *) + priv->simple_page.target); + priv->simple_page.target = NULL; + } + + if (priv->import != NULL) { + g_object_unref (priv->import); + priv->import = NULL; + } + + if (priv->fileuris != NULL) { + g_ptr_array_foreach (priv->fileuris, (GFunc) g_free, NULL); + g_ptr_array_free (priv->fileuris, TRUE); + priv->fileuris = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_import_assistant_parent_class)->dispose (object); +} + +static void +import_assistant_finalize (GObject *object) +{ + EImportAssistantPrivate *priv; + + priv = E_IMPORT_ASSISTANT_GET_PRIVATE (object); + + g_slist_free (priv->selection_page.importers); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_import_assistant_parent_class)->finalize (object); +} + +static gboolean +import_assistant_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + GtkWidgetClass *parent_class; + + if (event->keyval == GDK_KEY_Escape) { + g_signal_emit_by_name (widget, "cancel"); + return TRUE; + } + + /* Chain up to parent's key_press_event () method. */ + parent_class = GTK_WIDGET_CLASS (e_import_assistant_parent_class); + return parent_class->key_press_event (widget, event); +} + +static void +import_assistant_prepare (GtkAssistant *assistant, + GtkWidget *page) +{ + gint page_no = gtk_assistant_get_current_page (assistant); + gboolean is_simple = FALSE; + + g_object_get (assistant, "is-simple", &is_simple, NULL); + + if (is_simple) { + if (page_no == 0) { + prepare_simple_page (assistant, page); + } else if (page_no == 1) { + prepare_simple_destination_page (assistant, page); + } else if (page_no == 2) { + prepare_progress_page (assistant, page); + } + + return; + } + + switch (page_no) { + case PAGE_INTELI_SOURCE: + prepare_intelligent_page (assistant, page); + break; + case PAGE_FILE_CHOOSE: + prepare_file_page (assistant, page); + break; + case PAGE_FILE_DEST: + prepare_destination_page (assistant, page); + break; + case PAGE_PROGRESS: + prepare_progress_page (assistant, page); + break; + default: + break; + } +} + +static void +e_import_assistant_class_init (EImportAssistantClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkAssistantClass *assistant_class; + + g_type_class_add_private (class, sizeof (EImportAssistantPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = import_assistant_dispose; + object_class->finalize = import_assistant_finalize; + object_class->set_property = import_assistant_set_property; + object_class->get_property = import_assistant_get_property; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->key_press_event = import_assistant_key_press_event; + + assistant_class = GTK_ASSISTANT_CLASS (class); + assistant_class->prepare = import_assistant_prepare; + + g_object_class_install_property ( + object_class, + PROP_IS_SIMPLE, + g_param_spec_boolean ( + "is-simple", + NULL, + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + signals[FINISHED] = g_signal_new ( + "finished", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +import_assistant_construct (EImportAssistant *import_assistant) +{ + GtkAssistant *assistant; + GtkWidget *page; + + assistant = GTK_ASSISTANT (import_assistant); + + import_assistant->priv->import = + e_import_new ("org.gnome.evolution.shell.importer"); + + gtk_window_set_position (GTK_WINDOW (assistant), GTK_WIN_POS_CENTER); + gtk_window_set_title (GTK_WINDOW (assistant), _("Evolution Import Assistant")); + gtk_window_set_default_size (GTK_WINDOW (assistant), 500, 330); + + e_extensible_load_extensions (E_EXTENSIBLE (import_assistant)); + + if (import_assistant->priv->is_simple) { + /* simple import assistant page, URIs of files will be known later */ + page = import_assistant_simple_page_init (import_assistant); + + gtk_assistant_append_page (assistant, page); + gtk_assistant_set_page_title (assistant, page, _("Import Data")); + gtk_assistant_set_page_type (assistant, page, GTK_ASSISTANT_PAGE_CONTENT); + + /* File destination page - when with preview*/ + page = import_assistant_destination_page_init (import_assistant); + + gtk_assistant_append_page (assistant, page); + gtk_assistant_set_page_title (assistant, page, _("Import Location")); + gtk_assistant_set_page_type (assistant, page, GTK_ASSISTANT_PAGE_CONTENT); + } else { + /* complex import assistant pages */ + + /* Start page */ + page = gtk_label_new (""); + gtk_label_set_line_wrap (GTK_LABEL (page), TRUE); + gtk_misc_set_alignment (GTK_MISC (page), 0.0, 0.5); + gtk_misc_set_padding (GTK_MISC (page), 12, 12); + gtk_label_set_text (GTK_LABEL (page), _( + "Welcome to the Evolution Import Assistant.\n" + "With this assistant you will be guided through the " + "process of importing external files into Evolution.")); + gtk_widget_show (page); + + gtk_assistant_append_page (assistant, page); + gtk_assistant_set_page_title ( + assistant, page, _("Evolution Import Assistant")); + gtk_assistant_set_page_type ( + assistant, page, GTK_ASSISTANT_PAGE_INTRO); + gtk_assistant_set_page_complete (assistant, page, TRUE); + + /* Intelligent or direct import page */ + page = import_assistant_type_page_init (import_assistant); + + gtk_assistant_append_page (assistant, page); + gtk_assistant_set_page_title ( + assistant, page, _("Importer Type")); + gtk_assistant_set_page_type ( + assistant, page, GTK_ASSISTANT_PAGE_CONTENT); + gtk_assistant_set_page_complete (assistant, page, TRUE); + + /* Intelligent importer source page */ + page = import_assistant_selection_page_init (import_assistant); + + gtk_assistant_append_page (assistant, page); + gtk_assistant_set_page_title ( + assistant, page, _("Select Information to Import")); + gtk_assistant_set_page_type ( + assistant, page, GTK_ASSISTANT_PAGE_CONTENT); + + /* File selection and file type page */ + page = import_assistant_file_page_init (import_assistant); + + gtk_assistant_append_page (assistant, page); + gtk_assistant_set_page_title ( + assistant, page, _("Select a File")); + gtk_assistant_set_page_type ( + assistant, page, GTK_ASSISTANT_PAGE_CONTENT); + + /* File destination page */ + page = import_assistant_destination_page_init (import_assistant); + + gtk_assistant_append_page (assistant, page); + gtk_assistant_set_page_title ( + assistant, page, _("Import Location")); + gtk_assistant_set_page_type ( + assistant, page, GTK_ASSISTANT_PAGE_CONTENT); + + /* Finish page */ + page = gtk_label_new (""); + gtk_misc_set_alignment (GTK_MISC (page), 0.5, 0.5); + gtk_label_set_text ( + GTK_LABEL (page), _("Click \"Apply\" to " + "begin importing the file into Evolution.")); + gtk_widget_show (page); + + gtk_assistant_append_page (assistant, page); + gtk_assistant_set_page_title (assistant, page, _("Import Data")); + gtk_assistant_set_page_type (assistant, page, GTK_ASSISTANT_PAGE_CONFIRM); + gtk_assistant_set_page_complete (assistant, page, TRUE); + } + + /* Progress Page */ + page = import_assistant_progress_page_init (import_assistant); + + gtk_assistant_append_page (assistant, page); + gtk_assistant_set_page_title (assistant, page, _("Import Data")); + gtk_assistant_set_page_type (assistant, page, GTK_ASSISTANT_PAGE_PROGRESS); + gtk_assistant_set_page_complete (assistant, page, TRUE); + + gtk_assistant_set_forward_page_func ( + assistant, (GtkAssistantPageFunc) + forward_cb, import_assistant, NULL); + + gtk_assistant_update_buttons_state (assistant); +} + +static void +e_import_assistant_init (EImportAssistant *import_assistant) +{ + import_assistant->priv = + E_IMPORT_ASSISTANT_GET_PRIVATE (import_assistant); +} + +GtkWidget * +e_import_assistant_new (GtkWindow *parent) +{ + GtkWidget *assistant; + + assistant = g_object_new ( + E_TYPE_IMPORT_ASSISTANT, + "transient-for", parent, NULL); + + import_assistant_construct (E_IMPORT_ASSISTANT (assistant)); + + return assistant; +} + +/* Creates a simple assistant with only page to choose an import type + * and where to import, and then finishes. It shows import types based + * on the first valid URI given. + * + * Returns: EImportAssistant widget. + */ +GtkWidget * +e_import_assistant_new_simple (GtkWindow *parent, + const gchar * const *uris) +{ + GtkWidget *assistant; + + assistant = g_object_new ( + E_TYPE_IMPORT_ASSISTANT, + "transient-for", parent, + "is-simple", TRUE, + NULL); + + import_assistant_construct (E_IMPORT_ASSISTANT (assistant)); + + if (!set_import_uris (E_IMPORT_ASSISTANT (assistant), uris)) { + g_object_ref_sink (assistant); + g_object_unref (assistant); + return NULL; + } + + return assistant; +} diff --git a/e-util/e-import-assistant.h b/e-util/e-import-assistant.h new file mode 100644 index 0000000000..0ee580e31e --- /dev/null +++ b/e-util/e-import-assistant.h @@ -0,0 +1,72 @@ +/* + * e-import-assistant.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_IMPORT_ASSISTANT_H +#define E_IMPORT_ASSISTANT_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_IMPORT_ASSISTANT \ + (e_import_assistant_get_type ()) +#define E_IMPORT_ASSISTANT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_IMPORT_ASSISTANT, EImportAssistant)) +#define E_IMPORT_ASSISTANT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_IMPORT_ASSISTANT, EImportAssistantClass)) +#define E_IS_IMPORT_ASSISTANT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_IMPORT_ASSISTANT)) +#define E_IS_IMPORT_ASSISTANT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_IMPORT_ASSISTANT)) +#define E_IMPORT_ASSISTANT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_IMPORT_ASSISTANT, EImportAssistantClass)) + +G_BEGIN_DECLS + +typedef struct _EImportAssistant EImportAssistant; +typedef struct _EImportAssistantClass EImportAssistantClass; +typedef struct _EImportAssistantPrivate EImportAssistantPrivate; + +struct _EImportAssistant { + GtkAssistant parent; + EImportAssistantPrivate *priv; +}; + +struct _EImportAssistantClass { + GtkAssistantClass parent_class; +}; + +GType e_import_assistant_get_type (void); +GtkWidget * e_import_assistant_new (GtkWindow *parent); +GtkWidget * e_import_assistant_new_simple (GtkWindow *parent, + const gchar * const *uris); + +G_END_DECLS + +#endif /* E_IMPORT_ASSISTANT_H */ diff --git a/e-util/e-import.h b/e-util/e-import.h index 9d0eb5127a..69d40cfced 100644 --- a/e-util/e-import.h +++ b/e-util/e-import.h @@ -21,6 +21,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_IMPORT_H #define E_IMPORT_H @@ -244,7 +248,7 @@ EImportTargetHome * /* To implement a basic import plugin, you just need to subclass * this and initialise the class target type tables */ -#include "e-util/e-plugin.h" +#include <e-util/e-plugin.h> /* Standard GObject macros */ #define E_TYPE_IMPORT_HOOK \ diff --git a/e-util/e-interval-chooser.c b/e-util/e-interval-chooser.c new file mode 100644 index 0000000000..70e90bdf92 --- /dev/null +++ b/e-util/e-interval-chooser.c @@ -0,0 +1,214 @@ +/* + * e-interval-chooser.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-interval-chooser.h" + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "e-util-enums.h" + +#define E_INTERVAL_CHOOSER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_INTERVAL_CHOOSER, EIntervalChooserPrivate)) + +#define MINUTES_PER_HOUR (60) +#define MINUTES_PER_DAY (MINUTES_PER_HOUR * 24) + +struct _EIntervalChooserPrivate { + GtkComboBox *combo_box; /* not referenced */ + GtkSpinButton *spin_button; /* not referenced */ +}; + +enum { + PROP_0, + PROP_INTERVAL_MINUTES +}; + +G_DEFINE_TYPE ( + EIntervalChooser, + e_interval_chooser, + GTK_TYPE_BOX) + +static void +interval_chooser_notify_interval (GObject *object) +{ + g_object_notify (object, "interval-minutes"); +} + +static void +interval_chooser_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_INTERVAL_MINUTES: + e_interval_chooser_set_interval_minutes ( + E_INTERVAL_CHOOSER (object), + g_value_get_uint (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +interval_chooser_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_INTERVAL_MINUTES: + g_value_set_uint ( + value, + e_interval_chooser_get_interval_minutes ( + E_INTERVAL_CHOOSER (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +e_interval_chooser_class_init (EIntervalChooserClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EIntervalChooserPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = interval_chooser_set_property; + object_class->get_property = interval_chooser_get_property; + + g_object_class_install_property ( + object_class, + PROP_INTERVAL_MINUTES, + g_param_spec_uint ( + "interval-minutes", + "Interval in Minutes", + "Refresh interval in minutes", + 0, G_MAXUINT, 60, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_interval_chooser_init (EIntervalChooser *chooser) +{ + GtkWidget *widget; + + chooser->priv = E_INTERVAL_CHOOSER_GET_PRIVATE (chooser); + + gtk_orientable_set_orientation ( + GTK_ORIENTABLE (chooser), GTK_ORIENTATION_HORIZONTAL); + + gtk_box_set_spacing (GTK_BOX (chooser), 6); + + widget = gtk_spin_button_new_with_range (0, G_MAXUINT, 1); + gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (widget), TRUE); + gtk_spin_button_set_update_policy ( + GTK_SPIN_BUTTON (widget), GTK_UPDATE_IF_VALID); + gtk_box_pack_start (GTK_BOX (chooser), widget, TRUE, TRUE, 0); + chooser->priv->spin_button = GTK_SPIN_BUTTON (widget); + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "notify::value", + G_CALLBACK (interval_chooser_notify_interval), chooser); + + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (widget), _("minutes")); + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (widget), _("hours")); + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (widget), _("days")); + gtk_box_pack_start (GTK_BOX (chooser), widget, FALSE, FALSE, 0); + chooser->priv->combo_box = GTK_COMBO_BOX (widget); + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "notify::active", + G_CALLBACK (interval_chooser_notify_interval), chooser); +} + +GtkWidget * +e_interval_chooser_new (void) +{ + return g_object_new (E_TYPE_INTERVAL_CHOOSER, NULL); +} + +guint +e_interval_chooser_get_interval_minutes (EIntervalChooser *chooser) +{ + EDurationType units; + gdouble interval_minutes; + + g_return_val_if_fail (E_IS_SOURCE_CONFIG_REFRESH (chooser), 0); + + units = gtk_combo_box_get_active (chooser->priv->combo_box); + + interval_minutes = gtk_spin_button_get_value ( + chooser->priv->spin_button); + + switch (units) { + case E_DURATION_HOURS: + interval_minutes *= MINUTES_PER_HOUR; + break; + case E_DURATION_DAYS: + interval_minutes *= MINUTES_PER_DAY; + break; + default: + break; + } + + return (guint) interval_minutes; +} + +void +e_interval_chooser_set_interval_minutes (EIntervalChooser *chooser, + guint interval_minutes) +{ + EDurationType units; + + g_return_if_fail (E_IS_SOURCE_CONFIG_REFRESH (chooser)); + + if (interval_minutes == 0) { + units = E_DURATION_MINUTES; + } else if (interval_minutes % MINUTES_PER_DAY == 0) { + interval_minutes /= MINUTES_PER_DAY; + units = E_DURATION_DAYS; + } else if (interval_minutes % MINUTES_PER_HOUR == 0) { + interval_minutes /= MINUTES_PER_HOUR; + units = E_DURATION_HOURS; + } else { + units = E_DURATION_MINUTES; + } + + g_object_freeze_notify (G_OBJECT (chooser)); + + gtk_combo_box_set_active (chooser->priv->combo_box, units); + + gtk_spin_button_set_value ( + chooser->priv->spin_button, interval_minutes); + + g_object_thaw_notify (G_OBJECT (chooser)); +} diff --git a/e-util/e-interval-chooser.h b/e-util/e-interval-chooser.h new file mode 100644 index 0000000000..477ae1895c --- /dev/null +++ b/e-util/e-interval-chooser.h @@ -0,0 +1,72 @@ +/* + * e-interval-chooser.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_INTERVAL_CHOOSER_H +#define E_INTERVAL_CHOOSER_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_INTERVAL_CHOOSER \ + (e_interval_chooser_get_type ()) +#define E_INTERVAL_CHOOSER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_INTERVAL_CHOOSER, EIntervalChooser)) +#define E_INTERVAL_CHOOSER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_INTERVAL_CHOOSER, EIntervalChooserClass)) +#define E_IS_SOURCE_CONFIG_REFRESH(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_INTERVAL_CHOOSER)) +#define E_IS_SOURCE_CONFIG_REFRESH_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_INTERVAL_CHOOSER)) +#define E_INTERVAL_CHOOSER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_INTERVAL_CHOOSER, EIntervalChooserClass)) + +G_BEGIN_DECLS + +typedef struct _EIntervalChooser EIntervalChooser; +typedef struct _EIntervalChooserClass EIntervalChooserClass; +typedef struct _EIntervalChooserPrivate EIntervalChooserPrivate; + +struct _EIntervalChooser { + GtkBox parent; + EIntervalChooserPrivate *priv; +}; + +struct _EIntervalChooserClass { + GtkBoxClass parent_class; +}; + +GType e_interval_chooser_get_type (void) G_GNUC_CONST; +GtkWidget * e_interval_chooser_new (void); +guint e_interval_chooser_get_interval_minutes + (EIntervalChooser *refresh); +void e_interval_chooser_set_interval_minutes + (EIntervalChooser *refresh, + guint interval_minutes); + +G_END_DECLS + +#endif /* E_INTERVAL_CHOOSER_H */ diff --git a/e-util/e-mail-identity-combo-box.c b/e-util/e-mail-identity-combo-box.c new file mode 100644 index 0000000000..b76e0ee6bc --- /dev/null +++ b/e-util/e-mail-identity-combo-box.c @@ -0,0 +1,385 @@ +/* + * e-mail-identity-combo-box.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-mail-identity-combo-box.h" + +#define E_MAIL_IDENTITY_COMBO_BOX_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_MAIL_IDENTITY_COMBO_BOX, EMailIdentityComboBoxPrivate)) + +#define SOURCE_IS_MAIL_IDENTITY(source) \ + (e_source_has_extension ((source), E_SOURCE_EXTENSION_MAIL_IDENTITY)) + +struct _EMailIdentityComboBoxPrivate { + ESourceRegistry *registry; + guint refresh_idle_id; +}; + +enum { + PROP_0, + PROP_REGISTRY +}; + +enum { + COLUMN_DISPLAY_NAME, + COLUMN_UID +}; + +G_DEFINE_TYPE ( + EMailIdentityComboBox, + e_mail_identity_combo_box, + GTK_TYPE_COMBO_BOX) + +static gboolean +mail_identity_combo_box_refresh_idle_cb (EMailIdentityComboBox *combo_box) +{ + /* The refresh function will clear the idle ID. */ + e_mail_identity_combo_box_refresh (combo_box); + + return FALSE; +} + +static void +mail_identity_combo_box_registry_changed (ESourceRegistry *registry, + ESource *source, + EMailIdentityComboBox *combo_box) +{ + /* If the ESource in question has a "Mail Identity" extension, + * schedule a refresh of the tree model. Otherwise ignore it. + * We use an idle callback to limit how frequently we refresh + * the tree model, in case the registry is emitting lots of + * signals at once. */ + + if (!SOURCE_IS_MAIL_IDENTITY (source)) + return; + + if (combo_box->priv->refresh_idle_id > 0) + return; + + combo_box->priv->refresh_idle_id = g_idle_add ( + (GSourceFunc) mail_identity_combo_box_refresh_idle_cb, + combo_box); +} + +static void +mail_identity_combo_box_activate_default (EMailIdentityComboBox *combo_box) +{ + ESource *source; + ESourceRegistry *registry; + + registry = e_mail_identity_combo_box_get_registry (combo_box); + source = e_source_registry_ref_default_mail_identity (registry); + + if (source != NULL) { + const gchar *uid = e_source_get_uid (source); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (combo_box), uid); + g_object_unref (source); + } +} + +static void +mail_identity_combo_box_set_registry (EMailIdentityComboBox *combo_box, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (combo_box->priv->registry == NULL); + + combo_box->priv->registry = g_object_ref (registry); + + g_signal_connect ( + registry, "source-added", + G_CALLBACK (mail_identity_combo_box_registry_changed), + combo_box); + + g_signal_connect ( + registry, "source-changed", + G_CALLBACK (mail_identity_combo_box_registry_changed), + combo_box); + + g_signal_connect ( + registry, "source-removed", + G_CALLBACK (mail_identity_combo_box_registry_changed), + combo_box); +} + +static void +mail_identity_combo_box_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + mail_identity_combo_box_set_registry ( + E_MAIL_IDENTITY_COMBO_BOX (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_identity_combo_box_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + g_value_set_object ( + value, + e_mail_identity_combo_box_get_registry ( + E_MAIL_IDENTITY_COMBO_BOX (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_identity_combo_box_dispose (GObject *object) +{ + EMailIdentityComboBoxPrivate *priv; + + priv = E_MAIL_IDENTITY_COMBO_BOX_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_signal_handlers_disconnect_matched ( + priv->registry, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->refresh_idle_id > 0) { + g_source_remove (priv->refresh_idle_id); + priv->refresh_idle_id = 0; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_mail_identity_combo_box_parent_class)-> + dispose (object); +} + +static void +mail_identity_combo_box_constructed (GObject *object) +{ + GtkListStore *list_store; + GtkComboBox *combo_box; + GtkCellLayout *cell_layout; + GtkCellRenderer *cell_renderer; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_mail_identity_combo_box_parent_class)-> + constructed (object); + + combo_box = GTK_COMBO_BOX (object); + cell_layout = GTK_CELL_LAYOUT (object); + + list_store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING); + gtk_combo_box_set_model (combo_box, GTK_TREE_MODEL (list_store)); + gtk_combo_box_set_id_column (combo_box, COLUMN_UID); + g_object_unref (list_store); + + cell_renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (cell_layout, cell_renderer, TRUE); + gtk_cell_layout_add_attribute ( + cell_layout, cell_renderer, "text", COLUMN_DISPLAY_NAME); + + e_mail_identity_combo_box_refresh (E_MAIL_IDENTITY_COMBO_BOX (object)); +} + +static void +e_mail_identity_combo_box_class_init (EMailIdentityComboBoxClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private ( + class, sizeof (EMailIdentityComboBoxPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = mail_identity_combo_box_set_property; + object_class->get_property = mail_identity_combo_box_get_property; + object_class->dispose = mail_identity_combo_box_dispose; + object_class->constructed = mail_identity_combo_box_constructed; + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + NULL, + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_mail_identity_combo_box_init (EMailIdentityComboBox *combo_box) +{ + combo_box->priv = E_MAIL_IDENTITY_COMBO_BOX_GET_PRIVATE (combo_box); +} + +GtkWidget * +e_mail_identity_combo_box_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_MAIL_IDENTITY_COMBO_BOX, + "registry", registry, NULL); +} + +void +e_mail_identity_combo_box_refresh (EMailIdentityComboBox *combo_box) +{ + ESourceRegistry *registry; + GtkTreeModel *tree_model; + GtkComboBox *gtk_combo_box; + ESource *source; + GList *list, *link; + GHashTable *address_table; + const gchar *extension_name; + const gchar *saved_uid; + + g_return_if_fail (E_IS_MAIL_IDENTITY_COMBO_BOX (combo_box)); + + if (combo_box->priv->refresh_idle_id > 0) { + g_source_remove (combo_box->priv->refresh_idle_id); + combo_box->priv->refresh_idle_id = 0; + } + + gtk_combo_box = GTK_COMBO_BOX (combo_box); + tree_model = gtk_combo_box_get_model (gtk_combo_box); + + /* This is an interned string, which means it's safe + * to use even after clearing the combo box model. */ + saved_uid = gtk_combo_box_get_active_id (gtk_combo_box); + + gtk_list_store_clear (GTK_LIST_STORE (tree_model)); + + extension_name = E_SOURCE_EXTENSION_MAIL_IDENTITY; + registry = e_mail_identity_combo_box_get_registry (combo_box); + list = e_source_registry_list_sources (registry, extension_name); + + /* Build a hash table of GQueues by email address so we can + * spot duplicate email addresses. Then if the GQueue for a + * given email address has multiple elements, we use a more + * verbose description in the combo box. */ + + address_table = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_queue_free); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESourceMailIdentity *extension; + GQueue *queue; + const gchar *address; + + source = E_SOURCE (link->data); + extension = e_source_get_extension (source, extension_name); + address = e_source_mail_identity_get_address (extension); + + if (address == NULL) + continue; + + queue = g_hash_table_lookup (address_table, address); + if (queue == NULL) { + queue = g_queue_new (); + g_hash_table_insert ( + address_table, + g_strdup (address), queue); + } + + g_queue_push_tail (queue, source); + } + + for (link = list; link != NULL; link = g_list_next (link)) { + ESourceMailIdentity *extension; + GtkTreeIter iter; + GQueue *queue; + GString *string; + const gchar *address; + const gchar *display_name; + const gchar *name; + const gchar *uid; + + source = E_SOURCE (link->data); + + if (!e_source_registry_check_enabled (registry, source)) + continue; + + extension = e_source_get_extension (source, extension_name); + name = e_source_mail_identity_get_name (extension); + address = e_source_mail_identity_get_address (extension); + + if (name == NULL || address == NULL) + continue; + + queue = g_hash_table_lookup (address_table, address); + + display_name = e_source_get_display_name (source); + uid = e_source_get_uid (source); + + string = g_string_sized_new (512); + g_string_append_printf (string, "%s <%s>", name, address); + + /* Show the account name for duplicate email addresses. */ + if (queue != NULL && g_queue_get_length (queue) > 1) + g_string_append_printf (string, " (%s)", display_name); + + gtk_list_store_append (GTK_LIST_STORE (tree_model), &iter); + + gtk_list_store_set ( + GTK_LIST_STORE (tree_model), &iter, + COLUMN_DISPLAY_NAME, string->str, + COLUMN_UID, uid, -1); + + g_string_free (string, TRUE); + } + + g_hash_table_destroy (address_table); + + g_list_free_full (list, (GDestroyNotify) g_object_unref); + + /* Try and restore the previous selected source, or else pick + * the default identity of the default mail account. If even + * that fails, just pick the first item. */ + + if (saved_uid != NULL) + gtk_combo_box_set_active_id (gtk_combo_box, saved_uid); + + if (gtk_combo_box_get_active_id (gtk_combo_box) == NULL) + mail_identity_combo_box_activate_default (combo_box); + + if (gtk_combo_box_get_active_id (gtk_combo_box) == NULL) + gtk_combo_box_set_active (gtk_combo_box, 0); +} + +ESourceRegistry * +e_mail_identity_combo_box_get_registry (EMailIdentityComboBox *combo_box) +{ + g_return_val_if_fail (E_IS_MAIL_IDENTITY_COMBO_BOX (combo_box), NULL); + + return combo_box->priv->registry; +} diff --git a/e-util/e-mail-identity-combo-box.h b/e-util/e-mail-identity-combo-box.h new file mode 100644 index 0000000000..8c395b362f --- /dev/null +++ b/e-util/e-mail-identity-combo-box.h @@ -0,0 +1,75 @@ +/* + * e-mail-identity-combo-box.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MAIL_IDENTITY_COMBO_BOX_H +#define E_MAIL_IDENTITY_COMBO_BOX_H + +#include <gtk/gtk.h> +#include <libedataserver/libedataserver.h> + +/* Standard GObject macros */ +#define E_TYPE_MAIL_IDENTITY_COMBO_BOX \ + (e_mail_identity_combo_box_get_type ()) +#define E_MAIL_IDENTITY_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MAIL_IDENTITY_COMBO_BOX, EMailIdentityComboBox)) +#define E_MAIL_IDENTITY_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MAIL_IDENTITY_COMBO_BOX, EMailIdentityComboBoxClass)) +#define E_IS_MAIL_IDENTITY_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MAIL_IDENTITY_COMBO_BOX)) +#define E_IS_MAIL_IDENTITY_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MAIL_IDENTITY_COMBO_BOX)) +#define E_MAIL_IDENTITY_COMBO_BOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MAIL_IDENTITY_COMBO_BOX, EMailIdentityComboBoxClass)) + +G_BEGIN_DECLS + +typedef struct _EMailIdentityComboBox EMailIdentityComboBox; +typedef struct _EMailIdentityComboBoxClass EMailIdentityComboBoxClass; +typedef struct _EMailIdentityComboBoxPrivate EMailIdentityComboBoxPrivate; + +struct _EMailIdentityComboBox { + GtkComboBox parent; + EMailIdentityComboBoxPrivate *priv; +}; + +struct _EMailIdentityComboBoxClass { + GtkComboBoxClass parent_class; +}; + +GType e_mail_identity_combo_box_get_type + (void) G_GNUC_CONST; +GtkWidget * e_mail_identity_combo_box_new + (ESourceRegistry *registry); +void e_mail_identity_combo_box_refresh + (EMailIdentityComboBox *combo_box); +ESourceRegistry * + e_mail_identity_combo_box_get_registry + (EMailIdentityComboBox *combo_box); + +G_END_DECLS + +#endif /* E_MAIL_IDENTITY_COMBO_BOX_H */ diff --git a/e-util/e-mail-signature-combo-box.c b/e-util/e-mail-signature-combo-box.c new file mode 100644 index 0000000000..275c2538b9 --- /dev/null +++ b/e-util/e-mail-signature-combo-box.c @@ -0,0 +1,668 @@ +/* + * e-mail-signature-combo-box.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-mail-signature-combo-box.h" + +#include <config.h> +#include <glib/gi18n-lib.h> + +#define E_MAIL_SIGNATURE_COMBO_BOX_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_MAIL_SIGNATURE_COMBO_BOX, EMailSignatureComboBoxPrivate)) + +#define SOURCE_IS_MAIL_SIGNATURE(source) \ + (e_source_has_extension ((source), E_SOURCE_EXTENSION_MAIL_SIGNATURE)) + +struct _EMailSignatureComboBoxPrivate { + ESourceRegistry *registry; + guint refresh_idle_id; + gchar *identity_uid; +}; + +enum { + PROP_0, + PROP_IDENTITY_UID, + PROP_REGISTRY +}; + +enum { + COLUMN_DISPLAY_NAME, + COLUMN_UID +}; + +G_DEFINE_TYPE ( + EMailSignatureComboBox, + e_mail_signature_combo_box, + GTK_TYPE_COMBO_BOX) + +static gboolean +mail_signature_combo_box_refresh_idle_cb (EMailSignatureComboBox *combo_box) +{ + /* The refresh function will clear the idle ID. */ + e_mail_signature_combo_box_refresh (combo_box); + + return FALSE; +} + +static void +mail_signature_combo_box_registry_changed (ESourceRegistry *registry, + ESource *source, + EMailSignatureComboBox *combo_box) +{ + /* If the ESource in question has a "Mail Signature" extension, + * schedule a refresh of the tree model. Otherwise ignore it. + * We use an idle callback to limit how frequently we refresh + * the tree model, in case the registry is emitting lots of + * signals at once. */ + + if (!SOURCE_IS_MAIL_SIGNATURE (source)) + return; + + if (combo_box->priv->refresh_idle_id > 0) + return; + + combo_box->priv->refresh_idle_id = g_idle_add ( + (GSourceFunc) mail_signature_combo_box_refresh_idle_cb, + combo_box); +} + +static gboolean +mail_signature_combo_box_identity_to_signature (GBinding *binding, + const GValue *source_value, + GValue *target_value, + gpointer user_data) +{ + EMailSignatureComboBox *combo_box; + ESourceRegistry *registry; + GObject *source_object; + ESource *source; + ESourceMailIdentity *extension; + const gchar *identity_uid; + const gchar *signature_uid = "none"; + const gchar *extension_name; + + /* Source and target are the same object. */ + source_object = g_binding_get_source (binding); + combo_box = E_MAIL_SIGNATURE_COMBO_BOX (source_object); + registry = e_mail_signature_combo_box_get_registry (combo_box); + + identity_uid = g_value_get_string (source_value); + if (identity_uid == NULL) + return FALSE; + + source = e_source_registry_ref_source (registry, identity_uid); + if (source == NULL) + return FALSE; + + extension_name = E_SOURCE_EXTENSION_MAIL_IDENTITY; + if (!e_source_has_extension (source, extension_name)) { + g_object_unref (source); + return FALSE; + } + + extension = e_source_get_extension (source, extension_name); + signature_uid = e_source_mail_identity_get_signature_uid (extension); + g_value_set_string (target_value, signature_uid); + + g_object_unref (source); + + return TRUE; +} + +static void +mail_signature_combo_box_set_registry (EMailSignatureComboBox *combo_box, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (combo_box->priv->registry == NULL); + + combo_box->priv->registry = g_object_ref (registry); + + g_signal_connect ( + registry, "source-added", + G_CALLBACK (mail_signature_combo_box_registry_changed), + combo_box); + + g_signal_connect ( + registry, "source-changed", + G_CALLBACK (mail_signature_combo_box_registry_changed), + combo_box); + + g_signal_connect ( + registry, "source-removed", + G_CALLBACK (mail_signature_combo_box_registry_changed), + combo_box); +} + +static void +mail_signature_combo_box_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_IDENTITY_UID: + e_mail_signature_combo_box_set_identity_uid ( + E_MAIL_SIGNATURE_COMBO_BOX (object), + g_value_get_string (value)); + return; + + case PROP_REGISTRY: + mail_signature_combo_box_set_registry ( + E_MAIL_SIGNATURE_COMBO_BOX (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_combo_box_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_IDENTITY_UID: + g_value_set_string ( + value, + e_mail_signature_combo_box_get_identity_uid ( + E_MAIL_SIGNATURE_COMBO_BOX (object))); + return; + + case PROP_REGISTRY: + g_value_set_object ( + value, + e_mail_signature_combo_box_get_registry ( + E_MAIL_SIGNATURE_COMBO_BOX (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_combo_box_dispose (GObject *object) +{ + EMailSignatureComboBoxPrivate *priv; + + priv = E_MAIL_SIGNATURE_COMBO_BOX_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_signal_handlers_disconnect_matched ( + priv->registry, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->refresh_idle_id > 0) { + g_source_remove (priv->refresh_idle_id); + priv->refresh_idle_id = 0; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_mail_signature_combo_box_parent_class)-> + dispose (object); +} + +static void +mail_signature_combo_box_finalize (GObject *object) +{ + EMailSignatureComboBoxPrivate *priv; + + priv = E_MAIL_SIGNATURE_COMBO_BOX_GET_PRIVATE (object); + + g_free (priv->identity_uid); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_mail_signature_combo_box_parent_class)-> + finalize (object); +} + +static void +mail_signature_combo_box_constructed (GObject *object) +{ + GtkListStore *list_store; + GtkComboBox *combo_box; + GtkCellLayout *cell_layout; + GtkCellRenderer *cell_renderer; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_mail_signature_combo_box_parent_class)-> + constructed (object); + + combo_box = GTK_COMBO_BOX (object); + cell_layout = GTK_CELL_LAYOUT (object); + + list_store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING); + gtk_combo_box_set_model (combo_box, GTK_TREE_MODEL (list_store)); + gtk_combo_box_set_id_column (combo_box, COLUMN_UID); + g_object_unref (list_store); + + cell_renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (cell_layout, cell_renderer, TRUE); + gtk_cell_layout_add_attribute ( + cell_layout, cell_renderer, "text", COLUMN_DISPLAY_NAME); + + g_object_bind_property_full ( + combo_box, "identity-uid", + combo_box, "active-id", + G_BINDING_DEFAULT, + mail_signature_combo_box_identity_to_signature, + NULL, + NULL, (GDestroyNotify) NULL); + + e_mail_signature_combo_box_refresh ( + E_MAIL_SIGNATURE_COMBO_BOX (object)); +} + +static void +e_mail_signature_combo_box_class_init (EMailSignatureComboBoxClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private ( + class, sizeof (EMailSignatureComboBoxPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = mail_signature_combo_box_set_property; + object_class->get_property = mail_signature_combo_box_get_property; + object_class->dispose = mail_signature_combo_box_dispose; + object_class->finalize = mail_signature_combo_box_finalize; + object_class->constructed = mail_signature_combo_box_constructed; + + g_object_class_install_property ( + object_class, + PROP_IDENTITY_UID, + g_param_spec_string ( + "identity-uid", + "Identity UID", + NULL, + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + NULL, + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_mail_signature_combo_box_init (EMailSignatureComboBox *combo_box) +{ + combo_box->priv = E_MAIL_SIGNATURE_COMBO_BOX_GET_PRIVATE (combo_box); +} + +GtkWidget * +e_mail_signature_combo_box_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_MAIL_SIGNATURE_COMBO_BOX, + "registry", registry, NULL); +} + +void +e_mail_signature_combo_box_refresh (EMailSignatureComboBox *combo_box) +{ + ESourceRegistry *registry; + GtkComboBox *gtk_combo_box; + GtkTreeModel *tree_model; + GtkTreeIter iter; + ESource *source; + GList *list, *link; + const gchar *extension_name; + const gchar *saved_uid; + + g_return_if_fail (E_IS_MAIL_SIGNATURE_COMBO_BOX (combo_box)); + + if (combo_box->priv->refresh_idle_id > 0) { + g_source_remove (combo_box->priv->refresh_idle_id); + combo_box->priv->refresh_idle_id = 0; + } + + gtk_combo_box = GTK_COMBO_BOX (combo_box); + tree_model = gtk_combo_box_get_model (gtk_combo_box); + + /* This is an interned string, which means it's safe + * to use even after clearing the combo box model. */ + saved_uid = gtk_combo_box_get_active_id (gtk_combo_box); + + gtk_list_store_clear (GTK_LIST_STORE (tree_model)); + + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + registry = e_mail_signature_combo_box_get_registry (combo_box); + list = e_source_registry_list_sources (registry, extension_name); + + /* The "None" option always comes first. */ + + gtk_list_store_append (GTK_LIST_STORE (tree_model), &iter); + + gtk_list_store_set ( + GTK_LIST_STORE (tree_model), &iter, + COLUMN_DISPLAY_NAME, _("None"), + COLUMN_UID, "none", -1); + + /* The "autogenerated" UID has special meaning. */ + + gtk_list_store_append (GTK_LIST_STORE (tree_model), &iter); + + gtk_list_store_set ( + GTK_LIST_STORE (tree_model), &iter, + COLUMN_DISPLAY_NAME, _("Autogenerated"), + COLUMN_UID, E_MAIL_SIGNATURE_AUTOGENERATED_UID, -1); + + /* Followed by the other mail signatures, alphabetized. */ + + for (link = list; link != NULL; link = g_list_next (link)) { + GtkTreeIter iter; + const gchar *display_name; + const gchar *uid; + + source = E_SOURCE (link->data); + display_name = e_source_get_display_name (source); + uid = e_source_get_uid (source); + + gtk_list_store_append (GTK_LIST_STORE (tree_model), &iter); + + gtk_list_store_set ( + GTK_LIST_STORE (tree_model), &iter, + COLUMN_DISPLAY_NAME, display_name, + COLUMN_UID, uid, -1); + } + + g_list_free_full (list, (GDestroyNotify) g_object_unref); + + /* Try and restore the previous selected source, or else "None". */ + + if (saved_uid != NULL) + gtk_combo_box_set_active_id (gtk_combo_box, saved_uid); + + if (gtk_combo_box_get_active_id (gtk_combo_box) == NULL) + gtk_combo_box_set_active (gtk_combo_box, 0); +} + +ESourceRegistry * +e_mail_signature_combo_box_get_registry (EMailSignatureComboBox *combo_box) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_COMBO_BOX (combo_box), NULL); + + return combo_box->priv->registry; +} + +const gchar * +e_mail_signature_combo_box_get_identity_uid (EMailSignatureComboBox *combo_box) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_COMBO_BOX (combo_box), NULL); + + return combo_box->priv->identity_uid; +} + +void +e_mail_signature_combo_box_set_identity_uid (EMailSignatureComboBox *combo_box, + const gchar *identity_uid) +{ + const gchar *active_id; + + g_return_if_fail (E_IS_MAIL_SIGNATURE_COMBO_BOX (combo_box)); + + if (g_strcmp0 (combo_box->priv->identity_uid, identity_uid) == 0) + return; + + g_free (combo_box->priv->identity_uid); + combo_box->priv->identity_uid = g_strdup (identity_uid); + + g_object_notify (G_OBJECT (combo_box), "identity-uid"); + + /* If "Autogenerated" is selected, emit a "changed" signal as + * a hint to whomever is listening to reload the signature. */ + active_id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (combo_box)); + if (g_strcmp0 (active_id, E_MAIL_SIGNATURE_AUTOGENERATED_UID) == 0) + g_signal_emit_by_name (combo_box, "changed"); +} + +/**************** e_mail_signature_combo_box_load_selected() *****************/ + +typedef struct _LoadContext LoadContext; + +struct _LoadContext { + gchar *contents; + gsize length; + gboolean is_html; +}; + +static void +load_context_free (LoadContext *context) +{ + g_free (context->contents); + g_slice_free (LoadContext, context); +} + +static void +mail_signature_combo_box_autogenerate (EMailSignatureComboBox *combo_box, + LoadContext *context) +{ + ESourceMailIdentity *extension; + ESourceRegistry *registry; + ESource *source; + GString *buffer; + const gchar *extension_name; + const gchar *identity_uid; + const gchar *text; + gchar *escaped; + + identity_uid = e_mail_signature_combo_box_get_identity_uid (combo_box); + + /* If we have no mail identity UID, handle it as though + * "None" were selected. No need to report an error. */ + if (identity_uid == NULL) + return; + + registry = e_mail_signature_combo_box_get_registry (combo_box); + source = e_source_registry_ref_source (registry, identity_uid); + + /* If the mail identity lookup fails, handle it as though + * "None" were selected. No need to report an error. */ + if (source == NULL) + return; + + /* If the source is not actually a mail identity, handle it as + * though "None" were selected. No need to report an error. */ + extension_name = E_SOURCE_EXTENSION_MAIL_IDENTITY; + if (!e_source_has_extension (source, extension_name)) { + g_object_unref (source); + return; + } + + extension = e_source_get_extension (source, extension_name); + + /* The autogenerated signature format is: + * + * <NAME> <ADDRESS> + * <ORGANIZATION> + * + * The <ADDRESS> is a mailto link and + * the <ORGANIZATION> line is optional. + */ + + buffer = g_string_sized_new (512); + + text = e_source_mail_identity_get_name (extension); + escaped = (text != NULL) ? g_markup_escape_text (text, -1) : NULL; + if (escaped != NULL && *escaped != '\0') + g_string_append (buffer, escaped); + g_free (escaped); + + text = e_source_mail_identity_get_address (extension); + escaped = (text != NULL) ? g_markup_escape_text (text, -1) : NULL; + if (escaped != NULL && *escaped != '\0') + g_string_append_printf ( + buffer, " <<a href=\"mailto:%s\">%s</a>>", + escaped, escaped); + g_free (escaped); + + text = e_source_mail_identity_get_organization (extension); + escaped = (text != NULL) ? g_markup_escape_text (text, -1) : NULL; + if (escaped != NULL && *escaped != '\0') + g_string_append_printf (buffer, "<br>%s", escaped); + g_free (escaped); + + context->length = buffer->len; + context->contents = g_string_free (buffer, FALSE); + context->is_html = TRUE; + + g_object_unref (source); +} + +static void +mail_signature_combo_box_load_cb (ESource *source, + GAsyncResult *result, + GSimpleAsyncResult *simple) +{ + ESourceMailSignature *extension; + LoadContext *context; + const gchar *extension_name; + const gchar *mime_type; + GError *error = NULL; + + context = g_simple_async_result_get_op_res_gpointer (simple); + + e_source_mail_signature_load_finish ( + source, result, &context->contents, &context->length, &error); + + if (error != NULL) { + g_simple_async_result_set_from_error (simple, error); + g_simple_async_result_complete (simple); + g_object_unref (simple); + g_error_free (error); + return; + } + + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + extension = e_source_get_extension (source, extension_name); + mime_type = e_source_mail_signature_get_mime_type (extension); + context->is_html = (g_strcmp0 (mime_type, "text/html") == 0); + + g_simple_async_result_complete (simple); + + g_object_unref (simple); +} + +void +e_mail_signature_combo_box_load_selected (EMailSignatureComboBox *combo_box, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + ESourceRegistry *registry; + LoadContext *context; + ESource *source; + const gchar *active_id; + + g_return_if_fail (E_IS_MAIL_SIGNATURE_COMBO_BOX (combo_box)); + + context = g_slice_new0 (LoadContext); + + simple = g_simple_async_result_new ( + G_OBJECT (combo_box), callback, user_data, + e_mail_signature_combo_box_load_selected); + + g_simple_async_result_set_op_res_gpointer ( + simple, context, (GDestroyNotify) load_context_free); + + active_id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (combo_box)); + + if (active_id == NULL) { + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + return; + } + + if (g_strcmp0 (active_id, E_MAIL_SIGNATURE_AUTOGENERATED_UID) == 0) { + mail_signature_combo_box_autogenerate (combo_box, context); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + return; + } + + registry = e_mail_signature_combo_box_get_registry (combo_box); + source = e_source_registry_ref_source (registry, active_id); + + /* If for some reason the ESource lookup fails, handle it as + * though "None" were selected. No need to report an error. */ + if (source == NULL) { + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + return; + } + + e_source_mail_signature_load ( + source, io_priority, cancellable, (GAsyncReadyCallback) + mail_signature_combo_box_load_cb, simple); + + g_object_unref (source); +} + +gboolean +e_mail_signature_combo_box_load_selected_finish (EMailSignatureComboBox *combo_box, + GAsyncResult *result, + gchar **contents, + gsize *length, + gboolean *is_html, + GError **error) +{ + GSimpleAsyncResult *simple; + LoadContext *context; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (combo_box), + e_mail_signature_combo_box_load_selected), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + context = g_simple_async_result_get_op_res_gpointer (simple); + + if (g_simple_async_result_propagate_error (simple, error)) + return FALSE; + + if (contents != NULL) { + *contents = context->contents; + context->contents = NULL; + } + + if (length != NULL) + *length = context->length; + + if (is_html != NULL) + *is_html = context->is_html; + + return TRUE; +} diff --git a/e-util/e-mail-signature-combo-box.h b/e-util/e-mail-signature-combo-box.h new file mode 100644 index 0000000000..d39ba96e75 --- /dev/null +++ b/e-util/e-mail-signature-combo-box.h @@ -0,0 +1,95 @@ +/* + * e-mail-signature-combo-box.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MAIL_SIGNATURE_COMBO_BOX_H +#define E_MAIL_SIGNATURE_COMBO_BOX_H + +#include <gtk/gtk.h> +#include <libedataserver/libedataserver.h> + +/* Standard GObject macros */ +#define E_TYPE_MAIL_SIGNATURE_COMBO_BOX \ + (e_mail_signature_combo_box_get_type ()) +#define E_MAIL_SIGNATURE_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MAIL_SIGNATURE_COMBO_BOX, EMailSignatureComboBox)) +#define E_MAIL_SIGNATURE_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MAIL_SIGNATURE_COMBO_BOX, EMailSignatureComboBoxClass)) +#define E_IS_MAIL_SIGNATURE_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MAIL_SIGNATURE_COMBO_BOX)) +#define E_IS_MAIL_SIGNATURE_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MAIL_SIGNATURE_COMBO_BOX)) +#define E_MAIL_SIGNATURE_COMBO_BOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MAIL_SIGNATURE_COMBO_BOX, EMailSignatureComboBoxClass)) + +#define E_MAIL_SIGNATURE_AUTOGENERATED_UID "autogenerated" + +G_BEGIN_DECLS + +typedef struct _EMailSignatureComboBox EMailSignatureComboBox; +typedef struct _EMailSignatureComboBoxClass EMailSignatureComboBoxClass; +typedef struct _EMailSignatureComboBoxPrivate EMailSignatureComboBoxPrivate; + +struct _EMailSignatureComboBox { + GtkComboBox parent; + EMailSignatureComboBoxPrivate *priv; +}; + +struct _EMailSignatureComboBoxClass { + GtkComboBoxClass parent_class; +}; + +GType e_mail_signature_combo_box_get_type + (void) G_GNUC_CONST; +GtkWidget * e_mail_signature_combo_box_new + (ESourceRegistry *registry); +void e_mail_signature_combo_box_refresh + (EMailSignatureComboBox *combo_box); +ESourceRegistry * + e_mail_signature_combo_box_get_registry + (EMailSignatureComboBox *combo_box); +const gchar * e_mail_signature_combo_box_get_identity_uid + (EMailSignatureComboBox *combo_box); +void e_mail_signature_combo_box_set_identity_uid + (EMailSignatureComboBox *combo_box, + const gchar *identity_uid); +void e_mail_signature_combo_box_load_selected + (EMailSignatureComboBox *combo_box, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_mail_signature_combo_box_load_selected_finish + (EMailSignatureComboBox *combo_box, + GAsyncResult *result, + gchar **contents, + gsize *length, + gboolean *is_html, + GError **error); + +G_END_DECLS + +#endif /* E_MAIL_SIGNATURE_COMBO_BOX_H */ diff --git a/e-util/e-mail-signature-editor.c b/e-util/e-mail-signature-editor.c new file mode 100644 index 0000000000..961edf14ca --- /dev/null +++ b/e-util/e-mail-signature-editor.c @@ -0,0 +1,914 @@ +/* + * e-mail-signature-editor.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-mail-signature-editor.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "e-alert-bar.h" +#include "e-alert-dialog.h" +#include "e-alert-sink.h" +#include "e-web-view-gtkhtml.h" + +#define E_MAIL_SIGNATURE_EDITOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_MAIL_SIGNATURE_EDITOR, EMailSignatureEditorPrivate)) + +typedef struct _AsyncContext AsyncContext; + +struct _EMailSignatureEditorPrivate { + GtkActionGroup *action_group; + EFocusTracker *focus_tracker; + GCancellable *cancellable; + ESourceRegistry *registry; + ESource *source; + gchar *original_name; + + GtkWidget *entry; /* not referenced */ + GtkWidget *alert_bar; /* not referenced */ +}; + +struct _AsyncContext { + ESource *source; + GCancellable *cancellable; + gchar *contents; + gsize length; +}; + +enum { + PROP_0, + PROP_FOCUS_TRACKER, + PROP_REGISTRY, + PROP_SOURCE +}; + +static const gchar *ui = +"<ui>\n" +" <menubar name='main-menu'>\n" +" <placeholder name='pre-edit-menu'>\n" +" <menu action='file-menu'>\n" +" <menuitem action='save-and-close'/>\n" +" <separator/>" +" <menuitem action='close'/>\n" +" </menu>\n" +" </placeholder>\n" +" </menubar>\n" +" <toolbar name='main-toolbar'>\n" +" <placeholder name='pre-main-toolbar'>\n" +" <toolitem action='save-and-close'/>\n" +" </placeholder>\n" +" </toolbar>\n" +"</ui>"; + +/* Forward Declarations */ +static void e_mail_signature_editor_alert_sink_init + (EAlertSinkInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EMailSignatureEditor, + e_mail_signature_editor, + GTKHTML_TYPE_EDITOR, + G_IMPLEMENT_INTERFACE ( + E_TYPE_ALERT_SINK, + e_mail_signature_editor_alert_sink_init)) + +static void +async_context_free (AsyncContext *async_context) +{ + if (async_context->source != NULL) + g_object_unref (async_context->source); + + if (async_context->cancellable != NULL) + g_object_unref (async_context->cancellable); + + g_free (async_context->contents); + + g_slice_free (AsyncContext, async_context); +} + +static void +mail_signature_editor_loaded_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ESource *source; + EMailSignatureEditor *editor; + ESourceMailSignature *extension; + const gchar *extension_name; + const gchar *mime_type; + gchar *contents = NULL; + gboolean is_html; + GError *error = NULL; + + source = E_SOURCE (object); + editor = E_MAIL_SIGNATURE_EDITOR (user_data); + + e_source_mail_signature_load_finish ( + source, result, &contents, NULL, &error); + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warn_if_fail (contents == NULL); + g_object_unref (editor); + g_error_free (error); + return; + + } else if (error != NULL) { + g_warn_if_fail (contents == NULL); + e_alert_submit ( + E_ALERT_SINK (editor), + "widgets:no-load-signature", + error->message, NULL); + g_object_unref (editor); + g_error_free (error); + return; + } + + g_return_if_fail (contents != NULL); + + /* The load operation should have set the MIME type. */ + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + extension = e_source_get_extension (source, extension_name); + mime_type = e_source_mail_signature_get_mime_type (extension); + is_html = (g_strcmp0 (mime_type, "text/html") == 0); + + gtkhtml_editor_set_html_mode (GTKHTML_EDITOR (editor), is_html); + + if (is_html) { + gtkhtml_editor_insert_html ( + GTKHTML_EDITOR (editor), contents); + } else { + gtkhtml_editor_insert_text ( + GTKHTML_EDITOR (editor), contents); + + gtkhtml_editor_run_command (GTKHTML_EDITOR (editor), "cursor-position-save"); + gtkhtml_editor_run_command (GTKHTML_EDITOR (editor), "select-all"); + gtkhtml_editor_run_command (GTKHTML_EDITOR (editor), "style-pre"); + gtkhtml_editor_run_command (GTKHTML_EDITOR (editor), "unselect-all"); + gtkhtml_editor_run_command (GTKHTML_EDITOR (editor), "cursor-position-restore"); + } + + g_free (contents); + + g_object_unref (editor); +} + +static gboolean +mail_signature_editor_delete_event_cb (EMailSignatureEditor *editor, + GdkEvent *event) +{ + GtkActionGroup *action_group; + GtkAction *action; + + action_group = editor->priv->action_group; + action = gtk_action_group_get_action (action_group, "close"); + gtk_action_activate (action); + + return TRUE; +} + +static void +action_close_cb (GtkAction *action, + EMailSignatureEditor *editor) +{ + gboolean something_changed = FALSE; + const gchar *original_name; + const gchar *signature_name; + + original_name = editor->priv->original_name; + signature_name = gtk_entry_get_text (GTK_ENTRY (editor->priv->entry)); + + something_changed |= gtkhtml_editor_has_undo (GTKHTML_EDITOR (editor)); + something_changed |= (strcmp (signature_name, original_name) != 0); + + if (something_changed) { + gint response; + + response = e_alert_run_dialog_for_args ( + GTK_WINDOW (editor), + "widgets:ask-signature-changed", NULL); + if (response == GTK_RESPONSE_YES) { + GtkActionGroup *action_group; + + action_group = editor->priv->action_group; + action = gtk_action_group_get_action ( + action_group, "save-and-close"); + gtk_action_activate (action); + return; + } else if (response == GTK_RESPONSE_CANCEL) + return; + } + + gtk_widget_destroy (GTK_WIDGET (editor)); +} + +static void +action_save_and_close_cb (GtkAction *action, + EMailSignatureEditor *editor) +{ + GtkEntry *entry; + EAsyncClosure *closure; + GAsyncResult *result; + ESource *source; + gchar *display_name; + GError *error = NULL; + + entry = GTK_ENTRY (editor->priv->entry); + source = e_mail_signature_editor_get_source (editor); + + display_name = g_strstrip (g_strdup (gtk_entry_get_text (entry))); + + /* Make sure the signature name is not blank. */ + if (*display_name == '\0') { + e_alert_submit ( + E_ALERT_SINK (editor), + "widgets:blank-signature", NULL); + gtk_widget_grab_focus (GTK_WIDGET (entry)); + g_free (display_name); + return; + } + + e_source_set_display_name (source, display_name); + + g_free (display_name); + + /* Cancel any ongoing load or save operations. */ + if (editor->priv->cancellable != NULL) { + g_cancellable_cancel (editor->priv->cancellable); + g_object_unref (editor->priv->cancellable); + } + + editor->priv->cancellable = g_cancellable_new (); + + closure = e_async_closure_new (); + + e_mail_signature_editor_commit ( + editor, editor->priv->cancellable, + e_async_closure_callback, closure); + + result = e_async_closure_wait (closure); + + e_mail_signature_editor_commit_finish (editor, result, &error); + + e_async_closure_free (closure); + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_error_free (error); + + } else if (error != NULL) { + e_alert_submit ( + E_ALERT_SINK (editor), + "widgets:no-save-signature", + error->message, NULL); + g_error_free (error); + + /* Only destroy the editor if the save was successful. */ + } else { + gtk_widget_destroy (GTK_WIDGET (editor)); + } +} + +static GtkActionEntry entries[] = { + + { "close", + GTK_STOCK_CLOSE, + N_("_Close"), + "<Control>w", + N_("Close"), + G_CALLBACK (action_close_cb) }, + + { "save-and-close", + GTK_STOCK_SAVE, + N_("_Save and Close"), + "<Control>Return", + N_("Save and Close"), + G_CALLBACK (action_save_and_close_cb) }, + + { "file-menu", + NULL, + N_("_File"), + NULL, + NULL, + NULL } +}; + +static void +mail_signature_editor_set_registry (EMailSignatureEditor *editor, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (editor->priv->registry == NULL); + + editor->priv->registry = g_object_ref (registry); +} + +static void +mail_signature_editor_set_source (EMailSignatureEditor *editor, + ESource *source) +{ + GDBusObject *dbus_object = NULL; + const gchar *extension_name; + GError *error = NULL; + + g_return_if_fail (source == NULL || E_IS_SOURCE (source)); + g_return_if_fail (editor->priv->source == NULL); + + if (source != NULL) + dbus_object = e_source_ref_dbus_object (source); + + /* Clone the source so we can make changes to it freely. */ + editor->priv->source = e_source_new (dbus_object, NULL, &error); + + if (dbus_object != NULL) + g_object_unref (dbus_object); + + /* This should rarely fail. If the file was loaded successfully + * once then it should load successfully here as well, unless an + * I/O error occurs. */ + if (error != NULL) { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_error_free (error); + } + + /* Make sure the source has a mail signature extension. */ + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + e_source_get_extension (editor->priv->source, extension_name); +} + +static void +mail_signature_editor_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + mail_signature_editor_set_registry ( + E_MAIL_SIGNATURE_EDITOR (object), + g_value_get_object (value)); + return; + + case PROP_SOURCE: + mail_signature_editor_set_source ( + E_MAIL_SIGNATURE_EDITOR (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_editor_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_FOCUS_TRACKER: + g_value_set_object ( + value, + e_mail_signature_editor_get_focus_tracker ( + E_MAIL_SIGNATURE_EDITOR (object))); + return; + + case PROP_REGISTRY: + g_value_set_object ( + value, + e_mail_signature_editor_get_registry ( + E_MAIL_SIGNATURE_EDITOR (object))); + return; + + case PROP_SOURCE: + g_value_set_object ( + value, + e_mail_signature_editor_get_source ( + E_MAIL_SIGNATURE_EDITOR (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_editor_dispose (GObject *object) +{ + EMailSignatureEditorPrivate *priv; + + priv = E_MAIL_SIGNATURE_EDITOR_GET_PRIVATE (object); + + if (priv->action_group != NULL) { + g_object_unref (priv->action_group); + priv->action_group = NULL; + } + + if (priv->focus_tracker != NULL) { + g_object_unref (priv->focus_tracker); + priv->focus_tracker = NULL; + } + + if (priv->cancellable != NULL) { + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + if (priv->registry != NULL) { + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->source != NULL) { + g_object_unref (priv->source); + priv->source = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_mail_signature_editor_parent_class)-> + dispose (object); +} + +static void +mail_signature_editor_finalize (GObject *object) +{ + EMailSignatureEditorPrivate *priv; + + priv = E_MAIL_SIGNATURE_EDITOR_GET_PRIVATE (object); + + g_free (priv->original_name); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_mail_signature_editor_parent_class)-> + finalize (object); +} + +static void +mail_signature_editor_constructed (GObject *object) +{ + EMailSignatureEditor *editor; + GtkActionGroup *action_group; + EFocusTracker *focus_tracker; + GtkhtmlEditor *gtkhtml_editor; + GtkUIManager *ui_manager; + GDBusObject *dbus_object; + ESource *source; + GtkAction *action; + GtkWidget *container; + GtkWidget *widget; + const gchar *display_name; + GError *error = NULL; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_mail_signature_editor_parent_class)-> + constructed (object); + + editor = E_MAIL_SIGNATURE_EDITOR (object); + + gtkhtml_editor = GTKHTML_EDITOR (editor); + ui_manager = gtkhtml_editor_get_ui_manager (gtkhtml_editor); + + /* Because we are loading from a hard-coded string, there is + * no chance of I/O errors. Failure here implies a malformed + * UI definition. Full stop. */ + gtk_ui_manager_add_ui_from_string (ui_manager, ui, -1, &error); + if (error != NULL) + g_error ("%s", error->message); + + action_group = gtk_action_group_new ("signature"); + gtk_action_group_set_translation_domain ( + action_group, GETTEXT_PACKAGE); + gtk_action_group_add_actions ( + action_group, entries, + G_N_ELEMENTS (entries), editor); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + editor->priv->action_group = g_object_ref (action_group); + + /* Hide page properties because it is not inherited in the mail. */ + action = gtkhtml_editor_get_action (gtkhtml_editor, "properties-page"); + gtk_action_set_visible (action, FALSE); + + action = gtkhtml_editor_get_action ( + gtkhtml_editor, "context-properties-page"); + gtk_action_set_visible (action, FALSE); + + gtk_ui_manager_ensure_update (ui_manager); + + gtk_window_set_title (GTK_WINDOW (editor), _("Edit Signature")); + + /* Construct the signature name entry. */ + + container = gtkhtml_editor->vbox; + + widget = gtk_hbox_new (FALSE, 6); + gtk_container_set_border_width (GTK_CONTAINER (widget), 6); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + /* Position 2 should be between the main and style toolbars. */ + gtk_box_reorder_child (GTK_BOX (container), widget, 2); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_entry_new (); + gtk_box_pack_end (GTK_BOX (container), widget, TRUE, TRUE, 0); + editor->priv->entry = widget; /* not referenced */ + gtk_widget_show (widget); + + widget = gtk_label_new_with_mnemonic (_("_Signature Name:")); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), editor->priv->entry); + gtk_box_pack_end (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + g_signal_connect ( + editor, "delete-event", + G_CALLBACK (mail_signature_editor_delete_event_cb), NULL); + + /* Construct the alert bar for errors. */ + + container = gtkhtml_editor->vbox; + + widget = e_alert_bar_new (); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + /* Position 5 should be between the style toolbar and editing area. */ + gtk_box_reorder_child (GTK_BOX (container), widget, 5); + editor->priv->alert_bar = widget; /* not referenced */ + /* EAlertBar controls its own visibility. */ + + /* Configure an EFocusTracker to manage selection actions. + * + * XXX GtkhtmlEditor does not manage its own selection actions, + * which is technically a bug but works in our favor here + * because it won't cause any conflicts with EFocusTracker. */ + + focus_tracker = e_focus_tracker_new (GTK_WINDOW (editor)); + + action = gtkhtml_editor_get_action (gtkhtml_editor, "cut"); + e_focus_tracker_set_cut_clipboard_action (focus_tracker, action); + + action = gtkhtml_editor_get_action (gtkhtml_editor, "copy"); + e_focus_tracker_set_copy_clipboard_action (focus_tracker, action); + + action = gtkhtml_editor_get_action (gtkhtml_editor, "paste"); + e_focus_tracker_set_paste_clipboard_action (focus_tracker, action); + + action = gtkhtml_editor_get_action (gtkhtml_editor, "select-all"); + e_focus_tracker_set_select_all_action (focus_tracker, action); + + editor->priv->focus_tracker = focus_tracker; + + source = e_mail_signature_editor_get_source (editor); + + display_name = e_source_get_display_name (source); + if (display_name == NULL || *display_name == '\0') + display_name = _("Unnamed"); + + /* Set the entry text before we grab focus. */ + g_free (editor->priv->original_name); + editor->priv->original_name = g_strdup (display_name); + gtk_entry_set_text (GTK_ENTRY (editor->priv->entry), display_name); + + /* Set the focus appropriately. If this is a new signature, draw + * the user's attention to the signature name entry. Otherwise go + * straight to the editing area. */ + if (source == NULL) + gtk_widget_grab_focus (editor->priv->entry); + else { + GtkHTML *html; + + html = gtkhtml_editor_get_html (gtkhtml_editor); + gtk_widget_grab_focus (GTK_WIDGET (html)); + } + + /* Load file content only for an existing signature. + * (A new signature will not yet have a GDBusObject.) */ + dbus_object = e_source_ref_dbus_object (source); + if (dbus_object != NULL) { + GCancellable *cancellable; + + cancellable = g_cancellable_new (); + + e_source_mail_signature_load ( + source, + G_PRIORITY_DEFAULT, + cancellable, + mail_signature_editor_loaded_cb, + g_object_ref (editor)); + + g_warn_if_fail (editor->priv->cancellable == NULL); + editor->priv->cancellable = cancellable; + + g_object_unref (dbus_object); + } +} + +static void +mail_signature_editor_cut_clipboard (GtkhtmlEditor *editor) +{ + /* Do nothing. EFocusTracker handles this. */ +} + +static void +mail_signature_editor_copy_clipboard (GtkhtmlEditor *editor) +{ + /* Do nothing. EFocusTracker handles this. */ +} + +static void +mail_signature_editor_paste_clipboard (GtkhtmlEditor *editor) +{ + /* Do nothing. EFocusTracker handles this. */ +} + +static void +mail_signature_editor_select_all (GtkhtmlEditor *editor) +{ + /* Do nothing. EFocusTracker handles this. */ +} + +static void +mail_signature_editor_submit_alert (EAlertSink *alert_sink, + EAlert *alert) +{ + EMailSignatureEditorPrivate *priv; + EAlertBar *alert_bar; + GtkWidget *dialog; + GtkWindow *parent; + + priv = E_MAIL_SIGNATURE_EDITOR_GET_PRIVATE (alert_sink); + + switch (e_alert_get_message_type (alert)) { + case GTK_MESSAGE_INFO: + case GTK_MESSAGE_WARNING: + case GTK_MESSAGE_ERROR: + alert_bar = E_ALERT_BAR (priv->alert_bar); + e_alert_bar_add_alert (alert_bar, alert); + break; + + default: + parent = GTK_WINDOW (alert_sink); + dialog = e_alert_dialog_new (parent, alert); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + break; + } +} + +static void +e_mail_signature_editor_class_init (EMailSignatureEditorClass *class) +{ + GObjectClass *object_class; + GtkhtmlEditorClass *editor_class; + + g_type_class_add_private (class, sizeof (EMailSignatureEditorPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = mail_signature_editor_set_property; + object_class->get_property = mail_signature_editor_get_property; + object_class->dispose = mail_signature_editor_dispose; + object_class->finalize = mail_signature_editor_finalize; + object_class->constructed = mail_signature_editor_constructed; + + editor_class = GTKHTML_EDITOR_CLASS (class); + editor_class->cut_clipboard = mail_signature_editor_cut_clipboard; + editor_class->copy_clipboard = mail_signature_editor_copy_clipboard; + editor_class->paste_clipboard = mail_signature_editor_paste_clipboard; + editor_class->select_all = mail_signature_editor_select_all; + + g_object_class_install_property ( + object_class, + PROP_FOCUS_TRACKER, + g_param_spec_object ( + "focus-tracker", + NULL, + NULL, + E_TYPE_FOCUS_TRACKER, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + "Data source registry", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SOURCE, + g_param_spec_object ( + "source", + NULL, + NULL, + E_TYPE_SOURCE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_mail_signature_editor_alert_sink_init (EAlertSinkInterface *interface) +{ + interface->submit_alert = mail_signature_editor_submit_alert; +} + +static void +e_mail_signature_editor_init (EMailSignatureEditor *editor) +{ + editor->priv = E_MAIL_SIGNATURE_EDITOR_GET_PRIVATE (editor); +} + +GtkWidget * +e_mail_signature_editor_new (ESourceRegistry *registry, + ESource *source) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + if (source != NULL) + g_return_val_if_fail (E_IS_SOURCE (source), NULL); + + return g_object_new ( + E_TYPE_MAIL_SIGNATURE_EDITOR, + "html", e_web_view_gtkhtml_new (), + "registry", registry, + "source", source, NULL); +} + +EFocusTracker * +e_mail_signature_editor_get_focus_tracker (EMailSignatureEditor *editor) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_EDITOR (editor), NULL); + + return editor->priv->focus_tracker; +} + +ESourceRegistry * +e_mail_signature_editor_get_registry (EMailSignatureEditor *editor) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_EDITOR (editor), NULL); + + return editor->priv->registry; +} + +ESource * +e_mail_signature_editor_get_source (EMailSignatureEditor *editor) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_EDITOR (editor), NULL); + + return editor->priv->source; +} + +/********************** e_mail_signature_editor_commit() *********************/ + +static void +mail_signature_editor_replace_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + GError *error = NULL; + + simple = G_SIMPLE_ASYNC_RESULT (user_data); + + e_source_mail_signature_replace_finish ( + E_SOURCE (object), result, &error); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); + + g_simple_async_result_complete (simple); + + g_object_unref (simple); +} + +static void +mail_signature_editor_commit_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + GError *error = NULL; + + simple = G_SIMPLE_ASYNC_RESULT (user_data); + async_context = g_simple_async_result_get_op_res_gpointer (simple); + + e_source_registry_commit_source_finish ( + E_SOURCE_REGISTRY (object), result, &error); + + if (error != NULL) { + g_simple_async_result_take_error (simple, error); + g_simple_async_result_complete (simple); + g_object_unref (simple); + return; + } + + /* We can call this on our scratch source because only its UID is + * really needed, which even a new scratch source already knows. */ + e_source_mail_signature_replace ( + async_context->source, + async_context->contents, + async_context->length, + G_PRIORITY_DEFAULT, + async_context->cancellable, + mail_signature_editor_replace_cb, + simple); +} + +void +e_mail_signature_editor_commit (EMailSignatureEditor *editor, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + ESourceMailSignature *extension; + ESourceRegistry *registry; + ESource *source; + const gchar *extension_name; + const gchar *mime_type; + gchar *contents; + gboolean is_html; + gsize length; + + g_return_if_fail (E_IS_MAIL_SIGNATURE_EDITOR (editor)); + + registry = e_mail_signature_editor_get_registry (editor); + source = e_mail_signature_editor_get_source (editor); + is_html = gtkhtml_editor_get_html_mode (GTKHTML_EDITOR (editor)); + + if (is_html) { + mime_type = "text/html"; + contents = gtkhtml_editor_get_text_html ( + GTKHTML_EDITOR (editor), &length); + } else { + mime_type = "text/plain"; + contents = gtkhtml_editor_get_text_plain ( + GTKHTML_EDITOR (editor), &length); + } + + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + extension = e_source_get_extension (source, extension_name); + e_source_mail_signature_set_mime_type (extension, mime_type); + + async_context = g_slice_new0 (AsyncContext); + async_context->source = g_object_ref (source); + async_context->contents = contents; /* takes ownership */ + async_context->length = length; + + if (G_IS_CANCELLABLE (cancellable)) + async_context->cancellable = g_object_ref (cancellable); + + simple = g_simple_async_result_new ( + G_OBJECT (editor), callback, user_data, + e_mail_signature_editor_commit); + + g_simple_async_result_set_op_res_gpointer ( + simple, async_context, (GDestroyNotify) async_context_free); + + e_source_registry_commit_source ( + registry, source, + async_context->cancellable, + mail_signature_editor_commit_cb, + simple); +} + +gboolean +e_mail_signature_editor_commit_finish (EMailSignatureEditor *editor, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (editor), + e_mail_signature_editor_commit), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + + /* Assume success unless a GError is set. */ + return !g_simple_async_result_propagate_error (simple, error); +} + diff --git a/e-util/e-mail-signature-editor.h b/e-util/e-mail-signature-editor.h new file mode 100644 index 0000000000..c525d5a8a4 --- /dev/null +++ b/e-util/e-mail-signature-editor.h @@ -0,0 +1,87 @@ +/* + * e-mail-signature-editor.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MAIL_SIGNATURE_EDITOR_H +#define E_MAIL_SIGNATURE_EDITOR_H + +#include <gtkhtml-editor.h> +#include <libedataserver/libedataserver.h> + +#include <e-util/e-focus-tracker.h> + +/* Standard GObject macros */ +#define E_TYPE_MAIL_SIGNATURE_EDITOR \ + (e_mail_signature_editor_get_type ()) +#define E_MAIL_SIGNATURE_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MAIL_SIGNATURE_EDITOR, EMailSignatureEditor)) +#define E_MAIL_SIGNATURE_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MAIL_SIGNATURE_EDITOR, EMailSignatureEditorClass)) +#define E_IS_MAIL_SIGNATURE_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MAIL_SIGNATURE_EDITOR)) +#define E_IS_MAIL_SIGNATURE_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MAIL_SIGNATURE_EDITOR)) +#define E_MAIL_SIGNATURE_EDITOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MAIL_SIGNATURE_EDITOR, EMailSignatureEditorClass)) + +G_BEGIN_DECLS + +typedef struct _EMailSignatureEditor EMailSignatureEditor; +typedef struct _EMailSignatureEditorClass EMailSignatureEditorClass; +typedef struct _EMailSignatureEditorPrivate EMailSignatureEditorPrivate; + +struct _EMailSignatureEditor { + GtkhtmlEditor parent; + EMailSignatureEditorPrivate *priv; +}; + +struct _EMailSignatureEditorClass { + GtkhtmlEditorClass parent_class; +}; + +GType e_mail_signature_editor_get_type + (void) G_GNUC_CONST; +GtkWidget * e_mail_signature_editor_new (ESourceRegistry *registry, + ESource *source); +EFocusTracker * e_mail_signature_editor_get_focus_tracker + (EMailSignatureEditor *editor); +ESourceRegistry * + e_mail_signature_editor_get_registry + (EMailSignatureEditor *editor); +ESource * e_mail_signature_editor_get_source + (EMailSignatureEditor *editor); +void e_mail_signature_editor_commit (EMailSignatureEditor *editor, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_mail_signature_editor_commit_finish + (EMailSignatureEditor *editor, + GAsyncResult *result, + GError **error); + +G_END_DECLS + +#endif /* E_MAIL_SIGNATURE_EDITOR_H */ diff --git a/e-util/e-mail-signature-manager.c b/e-util/e-mail-signature-manager.c new file mode 100644 index 0000000000..66463336ea --- /dev/null +++ b/e-util/e-mail-signature-manager.c @@ -0,0 +1,708 @@ +/* + * e-mail-signature-manager.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-mail-signature-manager.h" + +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <gdk/gdkkeysyms.h> + +#include <libedataserver/libedataserver.h> + +#include "e-mail-signature-preview.h" +#include "e-mail-signature-tree-view.h" +#include "e-mail-signature-script-dialog.h" + +#define E_MAIL_SIGNATURE_MANAGER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_MAIL_SIGNATURE_MANAGER, EMailSignatureManagerPrivate)) + +#define PREVIEW_HEIGHT 200 + +struct _EMailSignatureManagerPrivate { + ESourceRegistry *registry; + + GtkWidget *tree_view; /* not referenced */ + GtkWidget *add_button; /* not referenced */ + GtkWidget *add_script_button; /* not referenced */ + GtkWidget *edit_button; /* not referenced */ + GtkWidget *remove_button; /* not referenced */ + GtkWidget *preview; /* not referenced */ + + gboolean prefer_html; +}; + +enum { + PROP_0, + PROP_PREFER_HTML, + PROP_REGISTRY +}; + +enum { + ADD_SIGNATURE, + ADD_SIGNATURE_SCRIPT, + EDITOR_CREATED, + EDIT_SIGNATURE, + REMOVE_SIGNATURE, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE ( + EMailSignatureManager, + e_mail_signature_manager, + GTK_TYPE_PANED) + +static void +mail_signature_manager_emit_editor_created (EMailSignatureManager *manager, + GtkWidget *editor) +{ + g_return_if_fail (E_IS_MAIL_SIGNATURE_EDITOR (editor)); + + g_signal_emit (manager, signals[EDITOR_CREATED], 0, editor); +} + +static gboolean +mail_signature_manager_key_press_event_cb (EMailSignatureManager *manager, + GdkEventKey *event) +{ + if (event->keyval == GDK_KEY_Delete) { + e_mail_signature_manager_remove_signature (manager); + return TRUE; + } + + return FALSE; +} + +static void +mail_signature_manager_run_script_dialog (EMailSignatureManager *manager, + ESource *source, + const gchar *title) +{ + ESourceRegistry *registry; + GtkWidget *dialog; + gpointer parent; + + registry = e_mail_signature_manager_get_registry (manager); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (manager)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + dialog = e_mail_signature_script_dialog_new (registry, parent, source); + gtk_window_set_title (GTK_WINDOW (dialog), title); + + if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_OK) { + EAsyncClosure *closure; + GAsyncResult *result; + GError *error = NULL; + + closure = e_async_closure_new (); + + /* FIXME Make this cancellable. */ + e_mail_signature_script_dialog_commit ( + E_MAIL_SIGNATURE_SCRIPT_DIALOG (dialog), NULL, + e_async_closure_callback, closure); + + result = e_async_closure_wait (closure); + + e_mail_signature_script_dialog_commit_finish ( + E_MAIL_SIGNATURE_SCRIPT_DIALOG (dialog), + result, &error); + + e_async_closure_free (closure); + + /* FIXME Make this into an EAlert. */ + if (error != NULL) { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_error_free (error); + } + } + + gtk_widget_destroy (dialog); +} + +static void +mail_signature_manager_selection_changed_cb (EMailSignatureManager *manager, + GtkTreeSelection *selection) +{ + EMailSignaturePreview *preview; + EMailSignatureTreeView *tree_view; + ESource *source; + GtkWidget *edit_button; + GtkWidget *remove_button; + gboolean sensitive; + const gchar *uid = NULL; + + edit_button = manager->priv->edit_button; + remove_button = manager->priv->remove_button; + + tree_view = E_MAIL_SIGNATURE_TREE_VIEW (manager->priv->tree_view); + source = e_mail_signature_tree_view_ref_selected_source (tree_view); + + if (source != NULL) + uid = e_source_get_uid (source); + + preview = E_MAIL_SIGNATURE_PREVIEW (manager->priv->preview); + e_mail_signature_preview_set_source_uid (preview, uid); + + sensitive = (source != NULL); + gtk_widget_set_sensitive (edit_button, sensitive); + gtk_widget_set_sensitive (remove_button, sensitive); + + if (source != NULL) + g_object_unref (source); +} + +static void +mail_signature_manager_set_registry (EMailSignatureManager *manager, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (manager->priv->registry == NULL); + + manager->priv->registry = g_object_ref (registry); +} + +static void +mail_signature_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_PREFER_HTML: + e_mail_signature_manager_set_prefer_html ( + E_MAIL_SIGNATURE_MANAGER (object), + g_value_get_boolean (value)); + return; + + case PROP_REGISTRY: + mail_signature_manager_set_registry ( + E_MAIL_SIGNATURE_MANAGER (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_PREFER_HTML: + g_value_set_boolean ( + value, + e_mail_signature_manager_get_prefer_html ( + E_MAIL_SIGNATURE_MANAGER (object))); + return; + + case PROP_REGISTRY: + g_value_set_object ( + value, + e_mail_signature_manager_get_registry ( + E_MAIL_SIGNATURE_MANAGER (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_manager_dispose (GObject *object) +{ + EMailSignatureManagerPrivate *priv; + + priv = E_MAIL_SIGNATURE_MANAGER_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_object_unref (priv->registry); + priv->registry = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_mail_signature_manager_parent_class)-> + dispose (object); +} + +static void +mail_signature_manager_constructed (GObject *object) +{ + EMailSignatureManager *manager; + GtkTreeSelection *selection; + ESourceRegistry *registry; + GSettings *settings; + GtkWidget *container; + GtkWidget *widget; + GtkWidget *hbox; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_mail_signature_manager_parent_class)-> + constructed (object); + + manager = E_MAIL_SIGNATURE_MANAGER (object); + registry = e_mail_signature_manager_get_registry (manager); + + gtk_orientable_set_orientation ( + GTK_ORIENTABLE (manager), GTK_ORIENTATION_VERTICAL); + + container = GTK_WIDGET (manager); + + widget = gtk_alignment_new (0.0, 0.0, 1.0, 1.0); + gtk_alignment_set_padding (GTK_ALIGNMENT (widget), 0, 12, 0, 0); + gtk_paned_pack1 (GTK_PANED (container), widget, TRUE, FALSE); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_hbox_new (FALSE, 6); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + container = hbox = widget; + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = e_mail_signature_tree_view_new (registry); + gtk_container_add (GTK_CONTAINER (container), widget); + manager->priv->tree_view = widget; /* not referenced */ + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "key-press-event", + G_CALLBACK (mail_signature_manager_key_press_event_cb), + manager); + + g_signal_connect_swapped ( + widget, "row-activated", + G_CALLBACK (e_mail_signature_manager_edit_signature), + manager); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget)); + + g_signal_connect_swapped ( + selection, "changed", + G_CALLBACK (mail_signature_manager_selection_changed_cb), + manager); + + container = hbox; + + widget = gtk_vbutton_box_new (); + gtk_button_box_set_layout ( + GTK_BUTTON_BOX (widget), GTK_BUTTONBOX_START); + gtk_box_set_spacing (GTK_BOX (widget), 6); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_button_new_from_stock (GTK_STOCK_ADD); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + manager->priv->add_button = widget; /* not referenced */ + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (e_mail_signature_manager_add_signature), + manager); + + widget = gtk_button_new_with_mnemonic (_("Add _Script")); + gtk_button_set_image ( + GTK_BUTTON (widget), gtk_image_new_from_stock ( + GTK_STOCK_EXECUTE, GTK_ICON_SIZE_BUTTON)); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + manager->priv->add_script_button = widget; /* not referenced */ + gtk_widget_show (widget); + + settings = g_settings_new ("org.gnome.desktop.lockdown"); + + g_settings_bind ( + settings, "disable-command-line", + widget, "visible", + G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_INVERT_BOOLEAN); + + g_object_unref (settings); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (e_mail_signature_manager_add_signature_script), + manager); + + widget = gtk_button_new_from_stock (GTK_STOCK_EDIT); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + manager->priv->edit_button = widget; /* not referenced */ + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (e_mail_signature_manager_edit_signature), + manager); + + widget = gtk_button_new_from_stock (GTK_STOCK_REMOVE); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + manager->priv->remove_button = widget; /* not referenced */ + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (e_mail_signature_manager_remove_signature), + manager); + + container = GTK_WIDGET (manager); + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_paned_pack2 (GTK_PANED (container), widget, FALSE, FALSE); + gtk_widget_show (widget); + + container = widget; + + widget = e_mail_signature_preview_new (registry); + gtk_container_add (GTK_CONTAINER (container), widget); + manager->priv->preview = widget; /* not referenced */ + gtk_widget_show (widget); + + gtk_paned_set_position (GTK_PANED (manager), PREVIEW_HEIGHT); +} + +static void +mail_signature_manager_add_signature (EMailSignatureManager *manager) +{ + ESourceRegistry *registry; + GtkWidget *editor; + + registry = e_mail_signature_manager_get_registry (manager); + + editor = e_mail_signature_editor_new (registry, NULL); + gtkhtml_editor_set_html_mode ( + GTKHTML_EDITOR (editor), manager->priv->prefer_html); + mail_signature_manager_emit_editor_created (manager, editor); + + gtk_widget_grab_focus (manager->priv->tree_view); +} + +static void +mail_signature_manager_add_signature_script (EMailSignatureManager *manager) +{ + const gchar *title; + + title = _("Add Signature Script"); + mail_signature_manager_run_script_dialog (manager, NULL, title); + + gtk_widget_grab_focus (manager->priv->tree_view); +} + +static void +mail_signature_manager_editor_created (EMailSignatureManager *manager, + EMailSignatureEditor *editor) +{ + GtkWindowPosition position; + gpointer parent; + + position = GTK_WIN_POS_CENTER_ON_PARENT; + parent = gtk_widget_get_toplevel (GTK_WIDGET (manager)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + gtk_window_set_transient_for (GTK_WINDOW (editor), parent); + gtk_window_set_position (GTK_WINDOW (editor), position); + gtk_widget_show (GTK_WIDGET (editor)); +} + +static void +mail_signature_manager_edit_signature (EMailSignatureManager *manager) +{ + EMailSignatureTreeView *tree_view; + ESourceMailSignature *extension; + ESourceRegistry *registry; + GtkWidget *editor; + ESource *source; + GFileInfo *file_info; + GFile *file; + const gchar *attribute; + const gchar *extension_name; + const gchar *title; + GError *error = NULL; + + registry = e_mail_signature_manager_get_registry (manager); + tree_view = E_MAIL_SIGNATURE_TREE_VIEW (manager->priv->tree_view); + source = e_mail_signature_tree_view_ref_selected_source (tree_view); + g_return_if_fail (source != NULL); + + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + extension = e_source_get_extension (source, extension_name); + file = e_source_mail_signature_get_file (extension); + + attribute = G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE; + + /* XXX This blocks but it should just be a local file. */ + file_info = g_file_query_info ( + file, attribute, G_FILE_QUERY_INFO_NONE, NULL, &error); + + /* FIXME Make this into an EAlert. */ + if (error != NULL) { + g_warn_if_fail (file_info == NULL); + g_warning ("%s: %s", G_STRFUNC, error->message); + g_object_unref (source); + g_error_free (error); + return; + } + + if (g_file_info_get_attribute_boolean (file_info, attribute)) + goto script; + + editor = e_mail_signature_editor_new (registry, source); + mail_signature_manager_emit_editor_created (manager, editor); + + goto exit; + +script: + title = _("Edit Signature Script"); + mail_signature_manager_run_script_dialog (manager, source, title); + +exit: + gtk_widget_grab_focus (GTK_WIDGET (tree_view)); + + g_object_unref (file_info); + g_object_unref (source); +} + +static void +mail_signature_manager_remove_signature (EMailSignatureManager *manager) +{ + EMailSignatureTreeView *tree_view; + ESourceMailSignature *extension; + ESource *source; + GFile *file; + const gchar *extension_name; + GError *error = NULL; + + tree_view = E_MAIL_SIGNATURE_TREE_VIEW (manager->priv->tree_view); + source = e_mail_signature_tree_view_ref_selected_source (tree_view); + + if (source == NULL) + return; + + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + extension = e_source_get_extension (source, extension_name); + + file = e_source_mail_signature_get_file (extension); + + /* XXX This blocks but it should just be a local file. */ + if (!g_file_delete (file, NULL, &error)) { + g_warning ("%s", error->message); + g_clear_error (&error); + } + + /* Remove the mail signature data source asynchronously. + * XXX No callback function because there's not much we can do + * if this fails. We should probably implement EAlertSink. */ + e_source_remove (source, NULL, NULL, NULL); + + gtk_widget_grab_focus (GTK_WIDGET (tree_view)); + + g_object_unref (source); +} + +static void +e_mail_signature_manager_class_init (EMailSignatureManagerClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private ( + class, sizeof (EMailSignatureManagerPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = mail_signature_manager_set_property; + object_class->get_property = mail_signature_manager_get_property; + object_class->dispose = mail_signature_manager_dispose; + object_class->constructed = mail_signature_manager_constructed; + + class->add_signature = mail_signature_manager_add_signature; + class->add_signature_script = + mail_signature_manager_add_signature_script; + class->editor_created = mail_signature_manager_editor_created; + class->edit_signature = mail_signature_manager_edit_signature; + class->remove_signature = mail_signature_manager_remove_signature; + + g_object_class_install_property ( + object_class, + PROP_PREFER_HTML, + g_param_spec_boolean ( + "prefer-html", + "Prefer HTML", + NULL, + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + NULL, + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + signals[ADD_SIGNATURE] = g_signal_new ( + "add-signature", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EMailSignatureManagerClass, add_signature), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[ADD_SIGNATURE_SCRIPT] = g_signal_new ( + "add-signature-script", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET ( + EMailSignatureManagerClass, add_signature_script), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[EDITOR_CREATED] = g_signal_new ( + "editor-created", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EMailSignatureManagerClass, editor_created), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + E_TYPE_MAIL_SIGNATURE_EDITOR); + + signals[EDIT_SIGNATURE] = g_signal_new ( + "edit-signature", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EMailSignatureManagerClass, edit_signature), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[REMOVE_SIGNATURE] = g_signal_new ( + "remove-signature", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EMailSignatureManagerClass, remove_signature), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_mail_signature_manager_init (EMailSignatureManager *manager) +{ + manager->priv = E_MAIL_SIGNATURE_MANAGER_GET_PRIVATE (manager); +} + +GtkWidget * +e_mail_signature_manager_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_MAIL_SIGNATURE_MANAGER, + "registry", registry, NULL); +} + +void +e_mail_signature_manager_add_signature (EMailSignatureManager *manager) +{ + g_return_if_fail (E_IS_MAIL_SIGNATURE_MANAGER (manager)); + + g_signal_emit (manager, signals[ADD_SIGNATURE], 0); +} + +void +e_mail_signature_manager_add_signature_script (EMailSignatureManager *manager) +{ + g_return_if_fail (E_IS_MAIL_SIGNATURE_MANAGER (manager)); + + g_signal_emit (manager, signals[ADD_SIGNATURE_SCRIPT], 0); +} + +void +e_mail_signature_manager_edit_signature (EMailSignatureManager *manager) +{ + g_return_if_fail (E_IS_MAIL_SIGNATURE_MANAGER (manager)); + + g_signal_emit (manager, signals[EDIT_SIGNATURE], 0); +} + +void +e_mail_signature_manager_remove_signature (EMailSignatureManager *manager) +{ + g_return_if_fail (E_IS_MAIL_SIGNATURE_MANAGER (manager)); + + g_signal_emit (manager, signals[REMOVE_SIGNATURE], 0); +} + +gboolean +e_mail_signature_manager_get_prefer_html (EMailSignatureManager *manager) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_MANAGER (manager), FALSE); + + return manager->priv->prefer_html; +} + +void +e_mail_signature_manager_set_prefer_html (EMailSignatureManager *manager, + gboolean prefer_html) +{ + g_return_if_fail (E_IS_MAIL_SIGNATURE_MANAGER (manager)); + + if (manager->priv->prefer_html == prefer_html) + return; + + manager->priv->prefer_html = prefer_html; + + g_object_notify (G_OBJECT (manager), "prefer-html"); +} + +ESourceRegistry * +e_mail_signature_manager_get_registry (EMailSignatureManager *manager) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_MANAGER (manager), NULL); + + return manager->priv->registry; +} diff --git a/e-util/e-mail-signature-manager.h b/e-util/e-mail-signature-manager.h new file mode 100644 index 0000000000..4b749c14a6 --- /dev/null +++ b/e-util/e-mail-signature-manager.h @@ -0,0 +1,93 @@ +/* + * e-mail-signature-manager.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MAIL_SIGNATURE_MANAGER_H +#define E_MAIL_SIGNATURE_MANAGER_H + +#include <gtk/gtk.h> + +#include <e-util/e-mail-signature-editor.h> +#include <e-util/e-mail-signature-tree-view.h> + +/* Standard GObject macros */ +#define E_TYPE_MAIL_SIGNATURE_MANAGER \ + (e_mail_signature_manager_get_type ()) +#define E_MAIL_SIGNATURE_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MAIL_SIGNATURE_MANAGER, EMailSignatureManager)) +#define E_MAIL_SIGNATURE_MANAGER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MAIL_SIGNATURE_MANAGER, EMailSignatureManagerClass)) +#define E_IS_MAIL_SIGNATURE_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MAIL_SIGNATURE_MANAGER)) +#define E_IS_MAIL_SIGNATURE_MANAGER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MAIL_SIGNATURE_MANAGER)) +#define E_MAIL_SIGNATURE_MANAGER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MAIL_SIGNATURE_MANAGER, EMailSignatureManagerClass)) + +G_BEGIN_DECLS + +typedef struct _EMailSignatureManager EMailSignatureManager; +typedef struct _EMailSignatureManagerClass EMailSignatureManagerClass; +typedef struct _EMailSignatureManagerPrivate EMailSignatureManagerPrivate; + +struct _EMailSignatureManager { + GtkPaned parent; + EMailSignatureManagerPrivate *priv; +}; + +struct _EMailSignatureManagerClass { + GtkPanedClass parent_class; + + void (*add_signature) (EMailSignatureManager *manager); + void (*add_signature_script) (EMailSignatureManager *manager); + void (*editor_created) (EMailSignatureManager *manager, + EMailSignatureEditor *editor); + void (*edit_signature) (EMailSignatureManager *manager); + void (*remove_signature) (EMailSignatureManager *manager); +}; + +GType e_mail_signature_manager_get_type + (void) G_GNUC_CONST; +GtkWidget * e_mail_signature_manager_new + (ESourceRegistry *registry); +void e_mail_signature_manager_add_signature + (EMailSignatureManager *manager); +void e_mail_signature_manager_add_signature_script + (EMailSignatureManager *manager); +void e_mail_signature_manager_edit_signature + (EMailSignatureManager *manager); +void e_mail_signature_manager_remove_signature + (EMailSignatureManager *manager); +gboolean e_mail_signature_manager_get_prefer_html + (EMailSignatureManager *manager); +void e_mail_signature_manager_set_prefer_html + (EMailSignatureManager *manager, + gboolean prefer_html); +ESourceRegistry * + e_mail_signature_manager_get_registry + (EMailSignatureManager *manager); + +#endif /* E_MAIL_SIGNATURE_MANAGER_H */ diff --git a/e-util/e-mail-signature-preview.c b/e-util/e-mail-signature-preview.c new file mode 100644 index 0000000000..8bf27fdb52 --- /dev/null +++ b/e-util/e-mail-signature-preview.c @@ -0,0 +1,358 @@ +/* + * e-mail-signature-preview.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-mail-signature-preview.h" + +#include <fcntl.h> +#include <string.h> +#include <unistd.h> +#include <glib/gstdio.h> + +#include "e-alert-sink.h" + +#define E_MAIL_SIGNATURE_PREVIEW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_MAIL_SIGNATURE_PREVIEW, EMailSignaturePreviewPrivate)) + +#define SOURCE_IS_MAIL_SIGNATURE(source) \ + (e_source_has_extension ((source), E_SOURCE_EXTENSION_MAIL_SIGNATURE)) + +struct _EMailSignaturePreviewPrivate { + ESourceRegistry *registry; + GCancellable *cancellable; + gchar *source_uid; +}; + +enum { + PROP_0, + PROP_REGISTRY, + PROP_SOURCE_UID +}; + +enum { + REFRESH, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE ( + EMailSignaturePreview, + e_mail_signature_preview, + E_TYPE_WEB_VIEW) + +static void +mail_signature_preview_load_cb (ESource *source, + GAsyncResult *result, + EMailSignaturePreview *preview) +{ + ESourceMailSignature *extension; + const gchar *extension_name; + const gchar *mime_type; + gchar *contents = NULL; + GError *error = NULL; + + e_source_mail_signature_load_finish ( + source, result, &contents, NULL, &error); + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warn_if_fail (contents == NULL); + g_object_unref (preview); + g_error_free (error); + return; + + } else if (error != NULL) { + g_warn_if_fail (contents == NULL); + e_alert_submit ( + E_ALERT_SINK (preview), + "widgets:no-load-signature", + error->message, NULL); + g_object_unref (preview); + g_error_free (error); + return; + } + + g_return_if_fail (contents != NULL); + + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + extension = e_source_get_extension (source, extension_name); + mime_type = e_source_mail_signature_get_mime_type (extension); + + if (g_strcmp0 (mime_type, "text/html") == 0) + e_web_view_load_string (E_WEB_VIEW (preview), contents); + else { + gchar *string; + + string = g_markup_printf_escaped ("<pre>%s</pre>", contents); + e_web_view_load_string (E_WEB_VIEW (preview), string); + g_free (string); + } + + g_free (contents); + + g_object_unref (preview); +} + +static void +mail_signature_preview_set_registry (EMailSignaturePreview *preview, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (preview->priv->registry == NULL); + + preview->priv->registry = g_object_ref (registry); +} + +static void +mail_signature_preview_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + mail_signature_preview_set_registry ( + E_MAIL_SIGNATURE_PREVIEW (object), + g_value_get_object (value)); + return; + + case PROP_SOURCE_UID: + e_mail_signature_preview_set_source_uid ( + E_MAIL_SIGNATURE_PREVIEW (object), + g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_preview_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + g_value_set_object ( + value, + e_mail_signature_preview_get_registry ( + E_MAIL_SIGNATURE_PREVIEW (object))); + return; + + case PROP_SOURCE_UID: + g_value_set_string ( + value, + e_mail_signature_preview_get_source_uid ( + E_MAIL_SIGNATURE_PREVIEW (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_preview_dispose (GObject *object) +{ + EMailSignaturePreviewPrivate *priv; + + priv = E_MAIL_SIGNATURE_PREVIEW_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->cancellable != NULL) { + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_mail_signature_preview_parent_class)-> + dispose (object); +} + +static void +mail_signature_preview_finalize (GObject *object) +{ + EMailSignaturePreviewPrivate *priv; + + priv = E_MAIL_SIGNATURE_PREVIEW_GET_PRIVATE (object); + + g_free (priv->source_uid); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_mail_signature_preview_parent_class)-> + finalize (object); +} + +static void +mail_signature_preview_refresh (EMailSignaturePreview *preview) +{ + ESourceRegistry *registry; + ESource *source; + const gchar *extension_name; + const gchar *source_uid; + + /* Cancel any unfinished refreshes. */ + if (preview->priv->cancellable != NULL) { + g_cancellable_cancel (preview->priv->cancellable); + g_object_unref (preview->priv->cancellable); + preview->priv->cancellable = NULL; + } + + source_uid = e_mail_signature_preview_get_source_uid (preview); + + if (source_uid == NULL) + goto fail; + + registry = e_mail_signature_preview_get_registry (preview); + source = e_source_registry_ref_source (registry, source_uid); + + if (source == NULL) + goto fail; + + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + if (!e_source_has_extension (source, extension_name)) { + g_object_unref (source); + goto fail; + } + + preview->priv->cancellable = g_cancellable_new (); + + e_source_mail_signature_load ( + source, G_PRIORITY_DEFAULT, + preview->priv->cancellable, (GAsyncReadyCallback) + mail_signature_preview_load_cb, g_object_ref (preview)); + + g_object_unref (source); + + return; + +fail: + e_web_view_clear (E_WEB_VIEW (preview)); +} + +static void +e_mail_signature_preview_class_init (EMailSignaturePreviewClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EMailSignaturePreviewPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = mail_signature_preview_set_property; + object_class->get_property = mail_signature_preview_get_property; + object_class->dispose = mail_signature_preview_dispose; + object_class->finalize = mail_signature_preview_finalize; + + class->refresh = mail_signature_preview_refresh; + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + NULL, + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SOURCE_UID, + g_param_spec_string ( + "source-uid", + "Source UID", + NULL, + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + signals[REFRESH] = g_signal_new ( + "refresh", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EMailSignaturePreviewClass, refresh), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_mail_signature_preview_init (EMailSignaturePreview *preview) +{ + preview->priv = E_MAIL_SIGNATURE_PREVIEW_GET_PRIVATE (preview); +} + +GtkWidget * +e_mail_signature_preview_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_MAIL_SIGNATURE_PREVIEW, + "registry", registry, NULL); +} + +void +e_mail_signature_preview_refresh (EMailSignaturePreview *preview) +{ + g_return_if_fail (E_IS_MAIL_SIGNATURE_PREVIEW (preview)); + + g_signal_emit (preview, signals[REFRESH], 0); +} + +ESourceRegistry * +e_mail_signature_preview_get_registry (EMailSignaturePreview *preview) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_PREVIEW (preview), NULL); + + return preview->priv->registry; +} + +const gchar * +e_mail_signature_preview_get_source_uid (EMailSignaturePreview *preview) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_PREVIEW (preview), NULL); + + return preview->priv->source_uid; +} + +void +e_mail_signature_preview_set_source_uid (EMailSignaturePreview *preview, + const gchar *source_uid) +{ + g_return_if_fail (E_IS_MAIL_SIGNATURE_PREVIEW (preview)); + + /* Avoid repeatedly loading the same signature file. */ + if (g_strcmp0 (source_uid, preview->priv->source_uid) == 0) + return; + + g_free (preview->priv->source_uid); + preview->priv->source_uid = g_strdup (source_uid); + + g_object_notify (G_OBJECT (preview), "source-uid"); + + e_mail_signature_preview_refresh (preview); +} diff --git a/e-util/e-mail-signature-preview.h b/e-util/e-mail-signature-preview.h new file mode 100644 index 0000000000..e7b0302e52 --- /dev/null +++ b/e-util/e-mail-signature-preview.h @@ -0,0 +1,84 @@ +/* + * e-mail-signature-preview.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MAIL_SIGNATURE_PREVIEW_H +#define E_MAIL_SIGNATURE_PREVIEW_H + +#include <libedataserver/libedataserver.h> + +#include <e-util/e-web-view.h> + +/* Standard GObject macros */ +#define E_TYPE_MAIL_SIGNATURE_PREVIEW \ + (e_mail_signature_preview_get_type ()) +#define E_MAIL_SIGNATURE_PREVIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MAIL_SIGNATURE_PREVIEW, EMailSignaturePreview)) +#define E_MAIL_SIGNATURE_PREVIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MAIL_SIGNATURE_PREVIEW, EMailSignaturePreviewClass)) +#define E_IS_MAIL_SIGNATURE_PREVIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MAIL_SIGNATURE_PREVIEW)) +#define E_IS_MAIL_SIGNATURE_PREVIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MAIL_SIGNATURE_PREVIEW)) +#define E_MAIL_SIGNATURE_PREVIEW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MAIL_SIGNATURE_PREVIEW, EMailSignaturePreview)) + +G_BEGIN_DECLS + +typedef struct _EMailSignaturePreview EMailSignaturePreview; +typedef struct _EMailSignaturePreviewClass EMailSignaturePreviewClass; +typedef struct _EMailSignaturePreviewPrivate EMailSignaturePreviewPrivate; + +struct _EMailSignaturePreview { + EWebView parent; + EMailSignaturePreviewPrivate *priv; +}; + +struct _EMailSignaturePreviewClass { + EWebViewClass parent_class; + + /* Signals */ + void (*refresh) (EMailSignaturePreview *preview); +}; + +GType e_mail_signature_preview_get_type + (void) G_GNUC_CONST; +GtkWidget * e_mail_signature_preview_new + (ESourceRegistry *registry); +void e_mail_signature_preview_refresh + (EMailSignaturePreview *preview); +ESourceRegistry * + e_mail_signature_preview_get_registry + (EMailSignaturePreview *preview); +const gchar * e_mail_signature_preview_get_source_uid + (EMailSignaturePreview *preview); +void e_mail_signature_preview_set_source_uid + (EMailSignaturePreview *preview, + const gchar *source_uid); + +G_END_DECLS + +#endif /* E_MAIL_SIGNATURE_PREVIEW_H */ diff --git a/e-util/e-mail-signature-script-dialog.c b/e-util/e-mail-signature-script-dialog.c new file mode 100644 index 0000000000..58e8c43157 --- /dev/null +++ b/e-util/e-mail-signature-script-dialog.c @@ -0,0 +1,731 @@ +/* + * e-mail-signature-script-dialog.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-mail-signature-script-dialog.h" + +#include <config.h> +#include <glib/gi18n-lib.h> + +#define E_MAIL_SIGNATURE_SCRIPT_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_MAIL_SIGNATURE_SCRIPT_DIALOG, \ + EMailSignatureScriptDialogPrivate)) + +typedef struct _AsyncContext AsyncContext; + +struct _EMailSignatureScriptDialogPrivate { + ESourceRegistry *registry; + ESource *source; + + GtkWidget *entry; /* not referenced */ + GtkWidget *file_chooser; /* not referenced */ + GtkWidget *alert; /* not referenced */ + + gchar *symlink_target; +}; + +struct _AsyncContext { + ESource *source; + GCancellable *cancellable; + gchar *symlink_target; +}; + +enum { + PROP_0, + PROP_REGISTRY, + PROP_SOURCE, + PROP_SYMLINK_TARGET +}; + +G_DEFINE_TYPE ( + EMailSignatureScriptDialog, + e_mail_signature_script_dialog, + GTK_TYPE_DIALOG) + +static void +async_context_free (AsyncContext *async_context) +{ + if (async_context->source != NULL) + g_object_unref (async_context->source); + + if (async_context->cancellable != NULL) + g_object_unref (async_context->cancellable); + + g_free (async_context->symlink_target); + + g_slice_free (AsyncContext, async_context); +} + +static gboolean +mail_signature_script_dialog_filter_cb (const GtkFileFilterInfo *filter_info) +{ + return g_file_test (filter_info->filename, G_FILE_TEST_IS_EXECUTABLE); +} + +static void +mail_signature_script_dialog_update_status (EMailSignatureScriptDialog *dialog) +{ + ESource *source; + const gchar *display_name; + const gchar *symlink_target; + gboolean show_alert; + gboolean sensitive; + + source = e_mail_signature_script_dialog_get_source (dialog); + + display_name = e_source_get_display_name (source); + sensitive = (display_name != NULL && *display_name != '\0'); + + symlink_target = + e_mail_signature_script_dialog_get_symlink_target (dialog); + + if (symlink_target != NULL) { + gboolean executable; + + executable = g_file_test ( + symlink_target, G_FILE_TEST_IS_EXECUTABLE); + + show_alert = !executable; + sensitive &= executable; + } else { + sensitive = FALSE; + show_alert = FALSE; + } + + if (show_alert) + gtk_widget_show (dialog->priv->alert); + else + gtk_widget_hide (dialog->priv->alert); + + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (dialog), GTK_RESPONSE_OK, sensitive); +} + +static void +mail_signature_script_dialog_file_set_cb (GtkFileChooserButton *button, + EMailSignatureScriptDialog *dialog) +{ + ESource *source; + ESourceMailSignature *extension; + GtkFileChooser *file_chooser; + const gchar *extension_name; + gchar *filename; + + file_chooser = GTK_FILE_CHOOSER (button); + filename = gtk_file_chooser_get_filename (file_chooser); + + g_free (dialog->priv->symlink_target); + dialog->priv->symlink_target = filename; /* takes ownership */ + + /* Invalidate the saved MIME type. */ + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + source = e_mail_signature_script_dialog_get_source (dialog); + extension = e_source_get_extension (source, extension_name); + e_source_mail_signature_set_mime_type (extension, NULL); + + g_object_notify (G_OBJECT (dialog), "symlink-target"); + + mail_signature_script_dialog_update_status (dialog); +} + +static void +mail_signature_script_dialog_query_cb (GFile *file, + GAsyncResult *result, + EMailSignatureScriptDialog *dialog) +{ + GFileInfo *file_info; + const gchar *symlink_target; + GError *error = NULL; + + file_info = g_file_query_info_finish (file, result, &error); + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warn_if_fail (file_info == NULL); + g_object_unref (dialog); + g_error_free (error); + return; + + } else if (error != NULL) { + g_warn_if_fail (file_info == NULL); + g_warning ("%s", error->message); + g_object_unref (dialog); + g_error_free (error); + return; + } + + g_return_if_fail (G_IS_FILE_INFO (file_info)); + + symlink_target = g_file_info_get_symlink_target (file_info); + + e_mail_signature_script_dialog_set_symlink_target ( + dialog, symlink_target); + + g_object_unref (file_info); + g_object_unref (dialog); +} + +static void +mail_signature_script_dialog_set_registry (EMailSignatureScriptDialog *dialog, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (dialog->priv->registry == NULL); + + dialog->priv->registry = g_object_ref (registry); +} + +static void +mail_signature_script_dialog_set_source (EMailSignatureScriptDialog *dialog, + ESource *source) +{ + GDBusObject *dbus_object = NULL; + const gchar *extension_name; + GError *error = NULL; + + g_return_if_fail (source == NULL || E_IS_SOURCE (source)); + g_return_if_fail (dialog->priv->source == NULL); + + if (source != NULL) + dbus_object = e_source_ref_dbus_object (source); + + /* Clone the source so we can make changes to it freely. */ + dialog->priv->source = e_source_new (dbus_object, NULL, &error); + + /* This should rarely fail. If the file was loaded successfully + * once then it should load successfully here as well, unless an + * I/O error occurs. */ + if (error != NULL) { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_error_free (error); + } + + /* Make sure the source has a mail signature extension. */ + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + e_source_get_extension (dialog->priv->source, extension_name); + + /* If we're editing an existing signature, query the symbolic + * link target of the signature file so we can initialize the + * file chooser button. Note: The asynchronous callback will + * run after the dialog initialization is complete. */ + if (dbus_object != NULL) { + ESourceMailSignature *extension; + const gchar *extension_name; + GFile *file; + + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + extension = e_source_get_extension (source, extension_name); + file = e_source_mail_signature_get_file (extension); + + g_file_query_info_async ( + file, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, + G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, + NULL, (GAsyncReadyCallback) + mail_signature_script_dialog_query_cb, + g_object_ref (dialog)); + + g_object_unref (dbus_object); + } +} + +static void +mail_signature_script_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + mail_signature_script_dialog_set_registry ( + E_MAIL_SIGNATURE_SCRIPT_DIALOG (object), + g_value_get_object (value)); + return; + + case PROP_SOURCE: + mail_signature_script_dialog_set_source ( + E_MAIL_SIGNATURE_SCRIPT_DIALOG (object), + g_value_get_object (value)); + return; + + case PROP_SYMLINK_TARGET: + e_mail_signature_script_dialog_set_symlink_target ( + E_MAIL_SIGNATURE_SCRIPT_DIALOG (object), + g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_script_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + g_value_set_object ( + value, + e_mail_signature_script_dialog_get_registry ( + E_MAIL_SIGNATURE_SCRIPT_DIALOG (object))); + return; + + case PROP_SOURCE: + g_value_set_object ( + value, + e_mail_signature_script_dialog_get_source ( + E_MAIL_SIGNATURE_SCRIPT_DIALOG (object))); + return; + + case PROP_SYMLINK_TARGET: + g_value_set_string ( + value, + e_mail_signature_script_dialog_get_symlink_target ( + E_MAIL_SIGNATURE_SCRIPT_DIALOG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_script_dialog_dispose (GObject *object) +{ + EMailSignatureScriptDialogPrivate *priv; + + priv = E_MAIL_SIGNATURE_SCRIPT_DIALOG_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->source != NULL) { + g_object_unref (priv->source); + priv->source = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_mail_signature_script_dialog_parent_class)-> + dispose (object); +} + +static void +mail_signature_script_dialog_finalize (GObject *object) +{ + EMailSignatureScriptDialogPrivate *priv; + + priv = E_MAIL_SIGNATURE_SCRIPT_DIALOG_GET_PRIVATE (object); + + g_free (priv->symlink_target); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_mail_signature_script_dialog_parent_class)-> + finalize (object); +} + +static void +mail_signature_script_dialog_constructed (GObject *object) +{ + EMailSignatureScriptDialog *dialog; + GtkFileFilter *filter; + GtkWidget *container; + GtkWidget *widget; + ESource *source; + const gchar *display_name; + gchar *markup; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_mail_signature_script_dialog_parent_class)-> + constructed (object); + + dialog = E_MAIL_SIGNATURE_SCRIPT_DIALOG (object); + + source = e_mail_signature_script_dialog_get_source (dialog); + display_name = e_source_get_display_name (source); + + gtk_container_set_border_width (GTK_CONTAINER (dialog), 5); + + gtk_dialog_add_button ( + GTK_DIALOG (dialog), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL); + + gtk_dialog_add_button ( + GTK_DIALOG (dialog), + GTK_STOCK_SAVE, GTK_RESPONSE_OK); + + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + + container = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + + widget = gtk_table_new (4, 2, FALSE); + gtk_table_set_col_spacings (GTK_TABLE (widget), 6); + gtk_table_set_row_spacings (GTK_TABLE (widget), 6); + gtk_table_set_row_spacing (GTK_TABLE (widget), 0, 12); + gtk_container_set_border_width (GTK_CONTAINER (widget), 5); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_image_new_from_stock ( + GTK_STOCK_DIALOG_INFO, GTK_ICON_SIZE_DIALOG); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, 0, 1, 0, 0, 0, 0); + gtk_widget_show (widget); + + widget = gtk_label_new (_( + "The output of this script will be used as your\n" + "signature. The name you specify will be used\n" + "for display purposes only.")); + gtk_table_attach ( + GTK_TABLE (container), widget, + 1, 2, 0, 1, GTK_FILL | GTK_EXPAND, 0, 0, 0); + gtk_widget_show (widget); + + widget = gtk_entry_new (); + gtk_entry_set_text (GTK_ENTRY (widget), display_name); + gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE); + gtk_table_attach ( + GTK_TABLE (container), widget, + 1, 2, 1, 2, GTK_FILL | GTK_EXPAND, 0, 0, 0); + dialog->priv->entry = widget; /* not referenced */ + gtk_widget_show (widget); + + g_object_bind_property ( + widget, "text", + source, "display-name", + G_BINDING_DEFAULT); + + widget = gtk_label_new_with_mnemonic (_("_Name:")); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->entry); + gtk_misc_set_alignment (GTK_MISC (widget), 1.0, 0.5); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, 1, 2, GTK_FILL, 0, 0, 0); + gtk_widget_show (widget); + + widget = gtk_file_chooser_button_new ( + NULL, GTK_FILE_CHOOSER_ACTION_OPEN); + gtk_table_attach ( + GTK_TABLE (container), widget, + 1, 2, 2, 3, GTK_FILL | GTK_EXPAND, 0, 0, 0); + dialog->priv->file_chooser = widget; /* not referenced */ + gtk_widget_show (widget); + + /* Restrict file selection to executable files. */ + filter = gtk_file_filter_new (); + gtk_file_filter_add_custom ( + filter, GTK_FILE_FILTER_FILENAME, + (GtkFileFilterFunc) mail_signature_script_dialog_filter_cb, + NULL, NULL); + gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (widget), filter); + + /* We create symbolic links to script files from the "signatures" + * directory, so restrict the selection to local files only. */ + gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (widget), TRUE); + + widget = gtk_label_new_with_mnemonic (_("S_cript:")); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->file_chooser); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, 2, 3, GTK_FILL, 0, 0, 0); + gtk_widget_show (widget); + + /* This is just a placeholder. */ + widget = gtk_label_new (NULL); + gtk_table_attach ( + GTK_TABLE (container), widget, + 0, 1, 3, 4, GTK_FILL, 0, 0, 0); + gtk_widget_show (widget); + + widget = gtk_hbox_new (FALSE, 6); + gtk_table_attach ( + GTK_TABLE (container), widget, + 1, 2, 3, 4, 0, 0, 0, 0); + dialog->priv->alert = widget; /* not referenced */ + gtk_widget_show (widget); + + container = widget; + + widget = gtk_image_new_from_stock ( + GTK_STOCK_DIALOG_WARNING, GTK_ICON_SIZE_MENU); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + markup = g_markup_printf_escaped ( + "<small>%s</small>", + _("Script file must be executable.")); + widget = gtk_label_new (markup); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + g_free (markup); + + g_signal_connect ( + dialog->priv->file_chooser, "file-set", + G_CALLBACK (mail_signature_script_dialog_file_set_cb), dialog); + + g_signal_connect_swapped ( + dialog->priv->entry, "changed", + G_CALLBACK (mail_signature_script_dialog_update_status), dialog); + + mail_signature_script_dialog_update_status (dialog); +} + +static void +e_mail_signature_script_dialog_class_init (EMailSignatureScriptDialogClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private ( + class, sizeof (EMailSignatureScriptDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = mail_signature_script_dialog_set_property; + object_class->get_property = mail_signature_script_dialog_get_property; + object_class->dispose = mail_signature_script_dialog_dispose; + object_class->finalize = mail_signature_script_dialog_finalize; + object_class->constructed = mail_signature_script_dialog_constructed; + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + "Data source registry", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SOURCE, + g_param_spec_object ( + "source", + "Source", + NULL, + E_TYPE_SOURCE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SYMLINK_TARGET, + g_param_spec_string ( + "symlink-target", + "Symlink Target", + NULL, + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_mail_signature_script_dialog_init (EMailSignatureScriptDialog *dialog) +{ + dialog->priv = E_MAIL_SIGNATURE_SCRIPT_DIALOG_GET_PRIVATE (dialog); +} + +GtkWidget * +e_mail_signature_script_dialog_new (ESourceRegistry *registry, + GtkWindow *parent, + ESource *source) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + if (source != NULL) + g_return_val_if_fail (E_IS_SOURCE (source), NULL); + + return g_object_new ( + E_TYPE_MAIL_SIGNATURE_SCRIPT_DIALOG, + "registry", registry, + "transient-for", parent, + "source", source, NULL); +} + +ESourceRegistry * +e_mail_signature_script_dialog_get_registry (EMailSignatureScriptDialog *dialog) +{ + g_return_val_if_fail ( + E_IS_MAIL_SIGNATURE_SCRIPT_DIALOG (dialog), NULL); + + return dialog->priv->registry; +} + +ESource * +e_mail_signature_script_dialog_get_source (EMailSignatureScriptDialog *dialog) +{ + g_return_val_if_fail ( + E_IS_MAIL_SIGNATURE_SCRIPT_DIALOG (dialog), NULL); + + return dialog->priv->source; +} + +const gchar * +e_mail_signature_script_dialog_get_symlink_target (EMailSignatureScriptDialog *dialog) +{ + g_return_val_if_fail ( + E_IS_MAIL_SIGNATURE_SCRIPT_DIALOG (dialog), NULL); + + return dialog->priv->symlink_target; +} + +void +e_mail_signature_script_dialog_set_symlink_target (EMailSignatureScriptDialog *dialog, + const gchar *symlink_target) +{ + GtkFileChooser *file_chooser; + + g_return_if_fail (E_IS_MAIL_SIGNATURE_SCRIPT_DIALOG (dialog)); + g_return_if_fail (symlink_target != NULL); + + g_free (dialog->priv->symlink_target); + dialog->priv->symlink_target = g_strdup (symlink_target); + + file_chooser = GTK_FILE_CHOOSER (dialog->priv->file_chooser); + gtk_file_chooser_set_filename (file_chooser, symlink_target); + + g_object_notify (G_OBJECT (dialog), "symlink-target"); + + mail_signature_script_dialog_update_status (dialog); +} + +/****************** e_mail_signature_script_dialog_commit() ******************/ + +static void +mail_signature_script_dialog_symlink_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + GError *error = NULL; + + simple = G_SIMPLE_ASYNC_RESULT (user_data); + + e_source_mail_signature_symlink_finish ( + E_SOURCE (object), result, &error); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); + + g_simple_async_result_complete (simple); + + g_object_unref (simple); +} + +static void +mail_signature_script_dialog_commit_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + GError *error = NULL; + + simple = G_SIMPLE_ASYNC_RESULT (user_data); + async_context = g_simple_async_result_get_op_res_gpointer (simple); + + e_source_registry_commit_source_finish ( + E_SOURCE_REGISTRY (object), result, &error); + + if (error != NULL) { + g_simple_async_result_take_error (simple, error); + g_simple_async_result_complete (simple); + g_object_unref (simple); + return; + } + + /* We can call this on our scratch source because only its UID is + * really needed, which even a new scratch source already knows. */ + e_source_mail_signature_symlink ( + async_context->source, + async_context->symlink_target, + G_PRIORITY_DEFAULT, + async_context->cancellable, + mail_signature_script_dialog_symlink_cb, + simple); +} + +void +e_mail_signature_script_dialog_commit (EMailSignatureScriptDialog *dialog, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + ESourceRegistry *registry; + ESource *source; + const gchar *symlink_target; + + g_return_if_fail (E_IS_MAIL_SIGNATURE_SCRIPT_DIALOG (dialog)); + + registry = e_mail_signature_script_dialog_get_registry (dialog); + source = e_mail_signature_script_dialog_get_source (dialog); + + symlink_target = + e_mail_signature_script_dialog_get_symlink_target (dialog); + + async_context = g_slice_new0 (AsyncContext); + async_context->source = g_object_ref (source); + async_context->symlink_target = g_strdup (symlink_target); + + if (G_IS_CANCELLABLE (cancellable)) + async_context->cancellable = g_object_ref (cancellable); + + simple = g_simple_async_result_new ( + G_OBJECT (dialog), callback, user_data, + e_mail_signature_script_dialog_commit); + + g_simple_async_result_set_op_res_gpointer ( + simple, async_context, (GDestroyNotify) async_context_free); + + e_source_registry_commit_source ( + registry, source, + async_context->cancellable, + mail_signature_script_dialog_commit_cb, + simple); +} + +gboolean +e_mail_signature_script_dialog_commit_finish (EMailSignatureScriptDialog *dialog, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (dialog), + e_mail_signature_script_dialog_commit), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + + /* Assume success unless a GError is set. */ + return !g_simple_async_result_propagate_error (simple, error); +} + diff --git a/e-util/e-mail-signature-script-dialog.h b/e-util/e-mail-signature-script-dialog.h new file mode 100644 index 0000000000..6a266b557d --- /dev/null +++ b/e-util/e-mail-signature-script-dialog.h @@ -0,0 +1,94 @@ +/* + * e-mail-signature-script-dialog.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MAIL_SIGNATURE_SCRIPT_DIALOG_H +#define E_MAIL_SIGNATURE_SCRIPT_DIALOG_H + +#include <gtk/gtk.h> +#include <libedataserver/libedataserver.h> + +/* Standard GObject macros */ +#define E_TYPE_MAIL_SIGNATURE_SCRIPT_DIALOG \ + (e_mail_signature_script_dialog_get_type ()) +#define E_MAIL_SIGNATURE_SCRIPT_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MAIL_SIGNATURE_SCRIPT_DIALOG, \ + EMailSignatureScriptDialog)) +#define E_MAIL_SIGNATURE_SCRIPT_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MAIL_SIGNATURE_SCRIPT_DIALOG, \ + EMailSignatureScriptDialogClass)) +#define E_IS_MAIL_SIGNATURE_SCRIPT_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MAIL_SIGNATURE_SCRIPT_DIALOG)) +#define E_IS_MAIL_SIGNATURE_SCRIPT_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MAIL_SIGNATURE_SCRIPT_DIALOG)) +#define E_MAIL_SIGNATURE_SCRIPT_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MAIL_SIGNATURE_SCRIPT_DIALOG, \ + EMailSignatureScriptDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EMailSignatureScriptDialog EMailSignatureScriptDialog; +typedef struct _EMailSignatureScriptDialogClass EMailSignatureScriptDialogClass; +typedef struct _EMailSignatureScriptDialogPrivate EMailSignatureScriptDialogPrivate; + +struct _EMailSignatureScriptDialog { + GtkDialog parent; + EMailSignatureScriptDialogPrivate *priv; +}; + +struct _EMailSignatureScriptDialogClass { + GtkDialogClass parent_class; +}; + +GType e_mail_signature_script_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_mail_signature_script_dialog_new + (ESourceRegistry *registry, + GtkWindow *parent, + ESource *source); +ESourceRegistry * + e_mail_signature_script_dialog_get_registry + (EMailSignatureScriptDialog *dialog); +ESource * e_mail_signature_script_dialog_get_source + (EMailSignatureScriptDialog *dialog); +const gchar * e_mail_signature_script_dialog_get_symlink_target + (EMailSignatureScriptDialog *dialog); +void e_mail_signature_script_dialog_set_symlink_target + (EMailSignatureScriptDialog *dialog, + const gchar *symlink_target); +void e_mail_signature_script_dialog_commit + (EMailSignatureScriptDialog *dialog, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_mail_signature_script_dialog_commit_finish + (EMailSignatureScriptDialog *dialog, + GAsyncResult *result, + GError **error); + +G_END_DECLS + +#endif /* E_MAIL_SIGNATURE_SCRIPT_DIALOG_H */ diff --git a/e-util/e-mail-signature-tree-view.c b/e-util/e-mail-signature-tree-view.c new file mode 100644 index 0000000000..05a2580d78 --- /dev/null +++ b/e-util/e-mail-signature-tree-view.c @@ -0,0 +1,395 @@ +/* + * e-mail-signature-tree-view.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-mail-signature-tree-view.h" + +#define E_MAIL_SIGNATURE_TREE_VIEW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_MAIL_SIGNATURE_TREE_VIEW, EMailSignatureTreeViewPrivate)) + +#define SOURCE_IS_MAIL_SIGNATURE(source) \ + (e_source_has_extension ((source), E_SOURCE_EXTENSION_MAIL_SIGNATURE)) + +struct _EMailSignatureTreeViewPrivate { + ESourceRegistry *registry; + guint refresh_idle_id; +}; + +enum { + PROP_0, + PROP_REGISTRY +}; + +enum { + COLUMN_DISPLAY_NAME, + COLUMN_UID, + NUM_COLUMNS +}; + +G_DEFINE_TYPE ( + EMailSignatureTreeView, + e_mail_signature_tree_view, + GTK_TYPE_TREE_VIEW) + +static gboolean +mail_signature_tree_view_refresh_idle_cb (EMailSignatureTreeView *tree_view) +{ + /* The refresh function will clear the idle ID. */ + e_mail_signature_tree_view_refresh (tree_view); + + return FALSE; +} + +static void +mail_signature_tree_view_registry_changed (ESourceRegistry *registry, + ESource *source, + EMailSignatureTreeView *tree_view) +{ + /* If the ESource in question has a "Mail Signature" extension, + * schedule a refresh of the tree model. Otherwise ignore it. + * We use an idle callback to limit how frequently we refresh + * the tree model, in case the registry is emitting lots of + * signals at once. */ + + if (!SOURCE_IS_MAIL_SIGNATURE (source)) + return; + + if (tree_view->priv->refresh_idle_id > 0) + return; + + tree_view->priv->refresh_idle_id = g_idle_add ( + (GSourceFunc) mail_signature_tree_view_refresh_idle_cb, + tree_view); +} + +static void +mail_signature_tree_view_set_registry (EMailSignatureTreeView *tree_view, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (tree_view->priv->registry == NULL); + + tree_view->priv->registry = g_object_ref (registry); + + g_signal_connect ( + registry, "source-added", + G_CALLBACK (mail_signature_tree_view_registry_changed), + tree_view); + + g_signal_connect ( + registry, "source-changed", + G_CALLBACK (mail_signature_tree_view_registry_changed), + tree_view); + + g_signal_connect ( + registry, "source-removed", + G_CALLBACK (mail_signature_tree_view_registry_changed), + tree_view); +} + +static void +mail_signature_tree_view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + mail_signature_tree_view_set_registry ( + E_MAIL_SIGNATURE_TREE_VIEW (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_tree_view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + g_value_set_object ( + value, + e_mail_signature_tree_view_get_registry ( + E_MAIL_SIGNATURE_TREE_VIEW (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +mail_signature_tree_view_dispose (GObject *object) +{ + EMailSignatureTreeViewPrivate *priv; + + priv = E_MAIL_SIGNATURE_TREE_VIEW_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_signal_handlers_disconnect_matched ( + priv->registry, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->refresh_idle_id > 0) { + g_source_remove (priv->refresh_idle_id); + priv->refresh_idle_id = 0; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_mail_signature_tree_view_parent_class)-> + dispose (object); +} + +static void +mail_signature_tree_view_constructed (GObject *object) +{ + GtkTreeView *tree_view; + GtkTreeViewColumn *column; + GtkCellRenderer *cell_renderer; + GtkListStore *list_store; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_mail_signature_tree_view_parent_class)-> + constructed (object); + + list_store = gtk_list_store_new ( + NUM_COLUMNS, + G_TYPE_STRING, /* COLUMN_DISPLAY_NAME */ + G_TYPE_STRING); /* COLUMN_UID */ + + tree_view = GTK_TREE_VIEW (object); + gtk_tree_view_set_headers_visible (tree_view, FALSE); + gtk_tree_view_set_model (tree_view, GTK_TREE_MODEL (list_store)); + + g_object_unref (list_store); + + /* Column: Signature Name */ + + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_expand (column, TRUE); + + cell_renderer = gtk_cell_renderer_text_new (); + g_object_set (cell_renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + gtk_tree_view_column_pack_start (column, cell_renderer, TRUE); + + gtk_tree_view_column_add_attribute ( + column, cell_renderer, "text", COLUMN_DISPLAY_NAME); + + gtk_tree_view_append_column (tree_view, column); + + e_mail_signature_tree_view_refresh ( + E_MAIL_SIGNATURE_TREE_VIEW (object)); +} + +static void +e_mail_signature_tree_view_class_init (EMailSignatureTreeViewClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private ( + class, sizeof (EMailSignatureTreeViewPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = mail_signature_tree_view_set_property; + object_class->get_property = mail_signature_tree_view_get_property; + object_class->dispose = mail_signature_tree_view_dispose; + object_class->constructed = mail_signature_tree_view_constructed; + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + NULL, + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_mail_signature_tree_view_init (EMailSignatureTreeView *tree_view) +{ + tree_view->priv = E_MAIL_SIGNATURE_TREE_VIEW_GET_PRIVATE (tree_view); +} + +GtkWidget * +e_mail_signature_tree_view_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_MAIL_SIGNATURE_TREE_VIEW, + "registry", registry, NULL); +} + +void +e_mail_signature_tree_view_refresh (EMailSignatureTreeView *tree_view) +{ + ESourceRegistry *registry; + GtkTreeModel *tree_model; + GtkTreeSelection *selection; + ESource *source; + GList *list, *link; + const gchar *extension_name; + gchar *saved_uid = NULL; + + g_return_if_fail (E_IS_MAIL_SIGNATURE_TREE_VIEW (tree_view)); + + if (tree_view->priv->refresh_idle_id > 0) { + g_source_remove (tree_view->priv->refresh_idle_id); + tree_view->priv->refresh_idle_id = 0; + } + + registry = e_mail_signature_tree_view_get_registry (tree_view); + tree_model = gtk_tree_view_get_model (GTK_TREE_VIEW (tree_view)); + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)); + + source = e_mail_signature_tree_view_ref_selected_source (tree_view); + if (source != NULL) { + saved_uid = e_source_dup_uid (source); + g_object_unref (source); + } + + gtk_list_store_clear (GTK_LIST_STORE (tree_model)); + + extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; + list = e_source_registry_list_sources (registry, extension_name); + + for (link = list; link != NULL; link = g_list_next (link)) { + GtkTreeIter iter; + const gchar *display_name; + const gchar *uid; + + source = E_SOURCE (link->data); + display_name = e_source_get_display_name (source); + uid = e_source_get_uid (source); + + gtk_list_store_append (GTK_LIST_STORE (tree_model), &iter); + + gtk_list_store_set ( + GTK_LIST_STORE (tree_model), &iter, + COLUMN_DISPLAY_NAME, display_name, + COLUMN_UID, uid, -1); + } + + g_list_free_full (list, (GDestroyNotify) g_object_unref); + + /* Try and restore the previous selected source. */ + + source = NULL; + + if (saved_uid != NULL) { + source = e_source_registry_ref_source (registry, saved_uid); + g_free (saved_uid); + } + + if (source != NULL) { + e_mail_signature_tree_view_set_selected_source ( + tree_view, source); + g_object_unref (source); + } + + /* Hint to refresh a signature preview. */ + g_signal_emit_by_name (selection, "changed"); +} + +ESourceRegistry * +e_mail_signature_tree_view_get_registry (EMailSignatureTreeView *tree_view) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_TREE_VIEW (tree_view), NULL); + + return tree_view->priv->registry; +} + +ESource * +e_mail_signature_tree_view_ref_selected_source (EMailSignatureTreeView *tree_view) +{ + ESourceRegistry *registry; + GtkTreeSelection *selection; + GtkTreeModel *tree_model; + GtkTreeIter iter; + ESource *source; + gchar *uid; + + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_TREE_VIEW (tree_view), NULL); + + registry = e_mail_signature_tree_view_get_registry (tree_view); + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)); + + if (!gtk_tree_selection_get_selected (selection, &tree_model, &iter)) + return NULL; + + gtk_tree_model_get (tree_model, &iter, COLUMN_UID, &uid, -1); + source = e_source_registry_ref_source (registry, uid); + g_free (uid); + + return source; +} + +void +e_mail_signature_tree_view_set_selected_source (EMailSignatureTreeView *tree_view, + ESource *source) +{ + ESourceRegistry *registry; + GtkTreeSelection *selection; + GtkTreeModel *tree_model; + GtkTreeIter iter; + gboolean valid; + + g_return_if_fail (E_IS_MAIL_SIGNATURE_TREE_VIEW (tree_view)); + g_return_if_fail (E_IS_SOURCE (source)); + + /* It is a programming error to pass an ESource that has no + * "Mail Signature" extension. */ + g_return_if_fail (SOURCE_IS_MAIL_SIGNATURE (source)); + + registry = e_mail_signature_tree_view_get_registry (tree_view); + tree_model = gtk_tree_view_get_model (GTK_TREE_VIEW (tree_view)); + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)); + + valid = gtk_tree_model_get_iter_first (tree_model, &iter); + + while (valid) { + ESource *candidate; + gchar *uid; + + gtk_tree_model_get (tree_model, &iter, COLUMN_UID, &uid, -1); + candidate = e_source_registry_ref_source (registry, uid); + g_free (uid); + + if (candidate != NULL && e_source_equal (source, candidate)) { + gtk_tree_selection_select_iter (selection, &iter); + g_object_unref (candidate); + break; + } + + if (candidate != NULL) + g_object_unref (candidate); + + valid = gtk_tree_model_iter_next (tree_model, &iter); + } +} diff --git a/e-util/e-mail-signature-tree-view.h b/e-util/e-mail-signature-tree-view.h new file mode 100644 index 0000000000..4da53291a2 --- /dev/null +++ b/e-util/e-mail-signature-tree-view.h @@ -0,0 +1,80 @@ +/* + * e-mail-signature-tree-view.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MAIL_SIGNATURE_TREE_VIEW_H +#define E_MAIL_SIGNATURE_TREE_VIEW_H + +#include <gtk/gtk.h> +#include <libedataserver/libedataserver.h> + +/* Standard GObject macros */ +#define E_TYPE_MAIL_SIGNATURE_TREE_VIEW \ + (e_mail_signature_tree_view_get_type ()) +#define E_MAIL_SIGNATURE_TREE_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MAIL_SIGNATURE_TREE_VIEW, EMailSignatureTreeView)) +#define E_MAIL_SIGNATURE_TREE_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MAIL_SIGNATURE_TREE_VIEW, EMailSignatureTreeViewClass)) +#define E_IS_MAIL_SIGNATURE_TREE_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MAIL_SIGNATURE_TREE_VIEW)) +#define E_IS_MAIL_SIGNATURE_TREE_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MAIL_SIGNATURE_TREE_VIEW)) +#define E_MAIL_SIGNATURE_TREE_VIEW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MAIL_SIGNATURE_TREE_VIEW, EMailSignatureTreeViewClass)) + +G_BEGIN_DECLS + +typedef struct _EMailSignatureTreeView EMailSignatureTreeView; +typedef struct _EMailSignatureTreeViewClass EMailSignatureTreeViewClass; +typedef struct _EMailSignatureTreeViewPrivate EMailSignatureTreeViewPrivate; + +struct _EMailSignatureTreeView { + GtkTreeView parent; + EMailSignatureTreeViewPrivate *priv; +}; + +struct _EMailSignatureTreeViewClass { + GtkTreeViewClass parent_class; +}; + +GType e_mail_signature_tree_view_get_type + (void) G_GNUC_CONST; +GtkWidget * e_mail_signature_tree_view_new + (ESourceRegistry *registry); +void e_mail_signature_tree_view_refresh + (EMailSignatureTreeView *tree_view); +ESourceRegistry * + e_mail_signature_tree_view_get_registry + (EMailSignatureTreeView *tree_view); +ESource * e_mail_signature_tree_view_ref_selected_source + (EMailSignatureTreeView *tree_view); +void e_mail_signature_tree_view_set_selected_source + (EMailSignatureTreeView *tree_view, + ESource *selected_source); + +G_END_DECLS + +#endif /* E_MAIL_SIGNATURE_TREE_VIEW_H */ diff --git a/e-util/e-map.c b/e-util/e-map.c new file mode 100644 index 0000000000..a419626b8d --- /dev/null +++ b/e-util/e-map.c @@ -0,0 +1,1429 @@ +/* + * Map widget. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Hans Petter Jansson <hpj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <math.h> +#include <stdlib.h> +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n.h> + +#include "e-map.h" + +#include "e-util-private.h" + +#define E_MAP_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_MAP, EMapPrivate)) + +#define E_MAP_TWEEN_TIMEOUT_MSECS 25 +#define E_MAP_TWEEN_DURATION_MSECS 150 + +/* Scroll step increment */ + +#define SCROLL_STEP_SIZE 32 + +/* */ + +#define E_MAP_GET_WIDTH(map) gtk_adjustment_get_upper((map)->priv->hadjustment) +#define E_MAP_GET_HEIGHT(map) gtk_adjustment_get_upper((map)->priv->vadjustment) + +/* Zoom state - keeps track of animation hacks */ + +typedef enum +{ + E_MAP_ZOOMED_IN, + E_MAP_ZOOMED_OUT, + E_MAP_ZOOMING_IN, + E_MAP_ZOOMING_OUT +} +EMapZoomState; + +/* The Tween struct used for zooming */ + +typedef struct _EMapTween EMapTween; + +struct _EMapTween { + guint start_time; + guint end_time; + gdouble longitude_offset; + gdouble latitude_offset; + gdouble zoom_factor; +}; + +/* Private part of the EMap structure */ + +struct _EMapPrivate { + /* Pointer to map image */ + GdkPixbuf *map_pixbuf; + cairo_surface_t *map_render_surface; + + /* Settings */ + gboolean frozen, smooth_zoom; + + /* Adjustments for scrolling */ + GtkAdjustment *hadjustment; + GtkAdjustment *vadjustment; + + /* GtkScrollablePolicy needs to be checked when + * driving the scrollable adjustment values */ + guint hscroll_policy : 1; + guint vscroll_policy : 1; + + /* Current scrolling offsets */ + gint xofs, yofs; + + /* Realtime zoom data */ + EMapZoomState zoom_state; + gdouble zoom_target_long, zoom_target_lat; + + /* Dots */ + GPtrArray *points; + + /* Tweens */ + GSList *tweens; + GTimer *timer; + guint timer_current_ms; + guint tween_id; +}; + +/* Properties */ + +enum { + PROP_0, + + /* For scrollable interface */ + PROP_HADJUSTMENT, + PROP_VADJUSTMENT, + PROP_HSCROLL_POLICY, + PROP_VSCROLL_POLICY +}; + +/* Internal prototypes */ + +static void update_render_surface (EMap *map, gboolean render_overlays); +static void set_scroll_area (EMap *map, gint width, gint height); +static void center_at (EMap *map, gdouble longitude, gdouble latitude); +static void scroll_to (EMap *map, gint x, gint y); +static gint load_map_background (EMap *map, gchar *name); +static void update_and_paint (EMap *map); +static void update_render_point (EMap *map, EMapPoint *point); +static void repaint_point (EMap *map, EMapPoint *point); + +/* ------ * + * Tweens * + * ------ */ + +static gboolean +e_map_is_tweening (EMap *map) +{ + return map->priv->timer != NULL; +} + +static void +e_map_stop_tweening (EMap *map) +{ + g_assert (map->priv->tweens == NULL); + + if (!e_map_is_tweening (map)) + return; + + g_timer_destroy (map->priv->timer); + map->priv->timer = NULL; + g_source_remove (map->priv->tween_id); + map->priv->tween_id = 0; +} + +static void +e_map_tween_destroy (EMap *map, + EMapTween *tween) +{ + map->priv->tweens = g_slist_remove (map->priv->tweens, tween); + g_slice_free (EMapTween, tween); + + if (map->priv->tweens == NULL) + e_map_stop_tweening (map); +} + +static gboolean +e_map_do_tween_cb (gpointer data) +{ + EMap *map = data; + GSList *walk; + + map->priv->timer_current_ms = + g_timer_elapsed (map->priv->timer, NULL) * 1000; + gtk_widget_queue_draw (GTK_WIDGET (map)); + + /* Can't use for loop here, because we need to advance + * the list before deleting. + */ + walk = map->priv->tweens; + while (walk) + { + EMapTween *tween = walk->data; + + walk = walk->next; + + if (tween->end_time <= map->priv->timer_current_ms) + e_map_tween_destroy (map, tween); + } + + return TRUE; +} + +static void +e_map_start_tweening (EMap *map) +{ + if (e_map_is_tweening (map)) + return; + + map->priv->timer = g_timer_new (); + map->priv->timer_current_ms = 0; + map->priv->tween_id = g_timeout_add ( + E_MAP_TWEEN_TIMEOUT_MSECS, e_map_do_tween_cb, map); + g_timer_start (map->priv->timer); +} + +static void +e_map_tween_new (EMap *map, + guint msecs, + gdouble longitude_offset, + gdouble latitude_offset, + gdouble zoom_factor) +{ + EMapTween *tween; + + if (!map->priv->smooth_zoom) + return; + + e_map_start_tweening (map); + + tween = g_slice_new (EMapTween); + + tween->start_time = map->priv->timer_current_ms; + tween->end_time = tween->start_time + msecs; + tween->longitude_offset = longitude_offset; + tween->latitude_offset = latitude_offset; + tween->zoom_factor = zoom_factor; + + map->priv->tweens = g_slist_prepend (map->priv->tweens, tween); + + gtk_widget_queue_draw (GTK_WIDGET (map)); +} + +G_DEFINE_TYPE_WITH_CODE ( + EMap, + e_map, + GTK_TYPE_WIDGET, + G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL)) + +static void +e_map_get_current_location (EMap *map, + gdouble *longitude, + gdouble *latitude) +{ + GtkAllocation allocation; + + gtk_widget_get_allocation (GTK_WIDGET (map), &allocation); + + e_map_window_to_world ( + map, allocation.width / 2.0, + allocation.height / 2.0, + longitude, latitude); +} + +static void +e_map_world_to_render_surface (EMap *map, + gdouble world_longitude, + gdouble world_latitude, + gdouble *win_x, + gdouble *win_y) +{ + gint width, height; + + width = E_MAP_GET_WIDTH (map); + height = E_MAP_GET_HEIGHT (map); + + *win_x = (width / 2.0 + (width / 2.0) * world_longitude / 180.0); + *win_y = (height / 2.0 - (height / 2.0) * world_latitude / 90.0); +} + +static void +e_map_tween_new_from (EMap *map, + guint msecs, + gdouble longitude, + gdouble latitude, + gdouble zoom) +{ + gdouble current_longitude, current_latitude; + + e_map_get_current_location ( + map, ¤t_longitude, ¤t_latitude); + + e_map_tween_new ( + map, msecs, + longitude - current_longitude, + latitude - current_latitude, + zoom / e_map_get_magnification (map)); +} + +static gdouble +e_map_get_tween_effect (EMap *map, + EMapTween *tween) +{ + gdouble elapsed; + + elapsed = (gdouble) + (map->priv->timer_current_ms - tween->start_time) / + tween->end_time; + + return MAX (0.0, 1.0 - elapsed); +} + +static void +e_map_apply_tween (EMapTween *tween, + gdouble effect, + gdouble *longitude, + gdouble *latitude, + gdouble *zoom) +{ + *zoom *= pow (tween->zoom_factor, effect); + *longitude += tween->longitude_offset * effect; + *latitude += tween->latitude_offset * effect; +} + +static void +e_map_tweens_compute_matrix (EMap *map, + cairo_matrix_t *matrix) +{ + GSList *walk; + gdouble zoom, x, y, latitude, longitude, effect; + GtkAllocation allocation; + + if (!e_map_is_tweening (map)) { + cairo_matrix_init_translate ( + matrix, -map->priv->xofs, -map->priv->yofs); + return; + } + + e_map_get_current_location (map, &longitude, &latitude); + zoom = 1.0; + + for (walk = map->priv->tweens; walk; walk = walk->next) { + EMapTween *tween = walk->data; + + effect = e_map_get_tween_effect (map, tween); + e_map_apply_tween (tween, effect, &longitude, &latitude, &zoom); + } + + gtk_widget_get_allocation (GTK_WIDGET (map), &allocation); + cairo_matrix_init_translate ( + matrix, + allocation.width / 2.0, + allocation.height / 2.0); + + e_map_world_to_render_surface (map, longitude, latitude, &x, &y); + cairo_matrix_scale (matrix, zoom, zoom); + cairo_matrix_translate (matrix, -x, -y); +} + +/* GtkScrollable implementation */ + +static void +e_map_adjustment_changed (GtkAdjustment *adjustment, + EMap *map) +{ + EMapPrivate *priv = map->priv; + + if (gtk_widget_get_realized (GTK_WIDGET (map))) { + gint hadj_value; + gint vadj_value; + + hadj_value = gtk_adjustment_get_value (priv->hadjustment); + vadj_value = gtk_adjustment_get_value (priv->vadjustment); + + scroll_to (map, hadj_value, vadj_value); + } +} + +static void +e_map_set_hadjustment_values (EMap *map) +{ + GtkAllocation allocation; + EMapPrivate *priv = map->priv; + GtkAdjustment *adj = priv->hadjustment; + gdouble old_value; + gdouble new_value; + gdouble new_upper; + + gtk_widget_get_allocation (GTK_WIDGET (map), &allocation); + + old_value = gtk_adjustment_get_value (adj); + new_upper = MAX (allocation.width, gdk_pixbuf_get_width (priv->map_pixbuf)); + + g_object_set ( + adj, + "lower", 0.0, + "upper", new_upper, + "page-size", (gdouble) allocation.height, + "step-increment", allocation.height * 0.1, + "page-increment", allocation.height * 0.9, + NULL); + + new_value = CLAMP (old_value, 0, new_upper - allocation.width); + if (new_value != old_value) + gtk_adjustment_set_value (adj, new_value); +} + +static void +e_map_set_vadjustment_values (EMap *map) +{ + GtkAllocation allocation; + EMapPrivate *priv = map->priv; + GtkAdjustment *adj = priv->vadjustment; + gdouble old_value; + gdouble new_value; + gdouble new_upper; + + gtk_widget_get_allocation (GTK_WIDGET (map), &allocation); + + old_value = gtk_adjustment_get_value (adj); + new_upper = MAX (allocation.height, gdk_pixbuf_get_height (priv->map_pixbuf)); + + g_object_set ( + adj, + "lower", 0.0, + "upper", new_upper, + "page-size", (gdouble) allocation.height, + "step-increment", allocation.height * 0.1, + "page-increment", allocation.height * 0.9, + NULL); + + new_value = CLAMP (old_value, 0, new_upper - allocation.height); + if (new_value != old_value) + gtk_adjustment_set_value (adj, new_value); +} + +static void +e_map_set_hadjustment (EMap *map, + GtkAdjustment *adjustment) +{ + EMapPrivate *priv = map->priv; + + if (adjustment && priv->hadjustment == adjustment) + return; + + if (priv->hadjustment != NULL) { + g_signal_handlers_disconnect_matched ( + priv->hadjustment, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, map); + g_object_unref (priv->hadjustment); + } + + if (!adjustment) + adjustment = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + g_signal_connect ( + adjustment, "value-changed", + G_CALLBACK (e_map_adjustment_changed), map); + priv->hadjustment = g_object_ref_sink (adjustment); + e_map_set_hadjustment_values (map); + + g_object_notify (G_OBJECT (map), "hadjustment"); +} + +static void +e_map_set_vadjustment (EMap *map, + GtkAdjustment *adjustment) +{ + EMapPrivate *priv = map->priv; + + if (adjustment && priv->vadjustment == adjustment) + return; + + if (priv->vadjustment != NULL) { + g_signal_handlers_disconnect_matched ( + priv->vadjustment, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, map); + g_object_unref (priv->vadjustment); + } + + if (!adjustment) + adjustment = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + g_signal_connect ( + adjustment, "value-changed", + G_CALLBACK (e_map_adjustment_changed), map); + priv->vadjustment = g_object_ref_sink (adjustment); + e_map_set_vadjustment_values (map); + + g_object_notify (G_OBJECT (map), "vadjustment"); +} + +/* ----------------- * + * Widget management * + * ----------------- */ + +static void +e_map_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + EMap *map; + + map = E_MAP (object); + + switch (property_id) { + case PROP_HADJUSTMENT: + e_map_set_hadjustment (map, g_value_get_object (value)); + break; + case PROP_VADJUSTMENT: + e_map_set_vadjustment (map, g_value_get_object (value)); + break; + case PROP_HSCROLL_POLICY: + map->priv->hscroll_policy = g_value_get_enum (value); + gtk_widget_queue_resize (GTK_WIDGET (map)); + break; + case PROP_VSCROLL_POLICY: + map->priv->vscroll_policy = g_value_get_enum (value); + gtk_widget_queue_resize (GTK_WIDGET (map)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +e_map_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EMap *map; + + map = E_MAP (object); + + switch (property_id) { + case PROP_HADJUSTMENT: + g_value_set_object (value, map->priv->hadjustment); + break; + case PROP_VADJUSTMENT: + g_value_set_object (value, map->priv->vadjustment); + break; + case PROP_HSCROLL_POLICY: + g_value_set_enum (value, map->priv->hscroll_policy); + break; + case PROP_VSCROLL_POLICY: + g_value_set_enum (value, map->priv->vscroll_policy); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +e_map_finalize (GObject *object) +{ + EMap *map; + + map = E_MAP (object); + + while (map->priv->tweens) + e_map_tween_destroy (map, map->priv->tweens->data); + e_map_stop_tweening (map); + + if (map->priv->map_pixbuf) { + g_object_unref (map->priv->map_pixbuf); + map->priv->map_pixbuf = NULL; + } + + /* gone in unrealize */ + g_assert (map->priv->map_render_surface == NULL); + + G_OBJECT_CLASS (e_map_parent_class)->finalize (object); +} + +static void +e_map_realize (GtkWidget *widget) +{ + GtkAllocation allocation; + GdkWindowAttr attr; + GdkWindow *window; + gint attr_mask; + + g_return_if_fail (widget != NULL); + g_return_if_fail (E_IS_MAP (widget)); + + gtk_widget_set_realized (widget, TRUE); + + gtk_widget_get_allocation (widget, &allocation); + + attr.window_type = GDK_WINDOW_CHILD; + attr.x = allocation.x; + attr.y = allocation.y; + attr.width = allocation.width; + attr.height = allocation.height; + attr.wclass = GDK_INPUT_OUTPUT; + attr.visual = gtk_widget_get_visual (widget); + attr.event_mask = gtk_widget_get_events (widget) | + GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | GDK_KEY_PRESS_MASK | + GDK_POINTER_MOTION_MASK; + + attr_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; + + window = gdk_window_new ( + gtk_widget_get_parent_window (widget), &attr, attr_mask); + gtk_widget_set_window (widget, window); + gdk_window_set_user_data (window, widget); + + update_render_surface (E_MAP (widget), TRUE); +} + +static void +e_map_unrealize (GtkWidget *widget) +{ + EMap *map = E_MAP (widget); + + cairo_surface_destroy (map->priv->map_render_surface); + map->priv->map_render_surface = NULL; + + if (GTK_WIDGET_CLASS (e_map_parent_class)->unrealize) + (*GTK_WIDGET_CLASS (e_map_parent_class)->unrealize) (widget); +} + +static void +e_map_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + EMap *map; + + g_return_if_fail (widget != NULL); + g_return_if_fail (E_IS_MAP (widget)); + + map = E_MAP (widget); + + /* TODO: Put real sizes here. */ + + *minimum = *natural = gdk_pixbuf_get_width (map->priv->map_pixbuf); +} + +static void +e_map_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + EMap *view; + EMapPrivate *priv; + + g_return_if_fail (widget != NULL); + g_return_if_fail (E_IS_MAP (widget)); + + view = E_MAP (widget); + priv = view->priv; + + /* TODO: Put real sizes here. */ + + *minimum = *natural = gdk_pixbuf_get_height (priv->map_pixbuf); +} + +static void +e_map_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + EMap *map; + + g_return_if_fail (widget != NULL); + g_return_if_fail (E_IS_MAP (widget)); + g_return_if_fail (allocation != NULL); + + map = E_MAP (widget); + + /* Resize the window */ + + gtk_widget_set_allocation (widget, allocation); + + if (gtk_widget_get_realized (widget)) { + GdkWindow *window; + + window = gtk_widget_get_window (widget); + + gdk_window_move_resize ( + window, allocation->x, allocation->y, + allocation->width, allocation->height); + + gtk_widget_queue_draw (widget); + } + + update_render_surface (map, TRUE); +} + +static gboolean +e_map_draw (GtkWidget *widget, + cairo_t *cr) +{ + EMap *map; + cairo_matrix_t matrix; + + if (!gtk_widget_is_drawable (widget)) + return FALSE; + + map = E_MAP (widget); + + cairo_save (cr); + + e_map_tweens_compute_matrix (map, &matrix); + cairo_transform (cr, &matrix); + + cairo_set_source_surface (cr, map->priv->map_render_surface, 0, 0); + cairo_paint (cr); + + cairo_restore (cr); + + return FALSE; +} + +static gint +e_map_button_press (GtkWidget *widget, + GdkEventButton *event) +{ + if (!gtk_widget_has_focus (widget)) + gtk_widget_grab_focus (widget); + + return TRUE; +} + +static gint +e_map_button_release (GtkWidget *widget, + GdkEventButton *event) +{ + if (event->button != 1) + return FALSE; + + gdk_device_ungrab (event->device, event->time); + return TRUE; +} + +static gint +e_map_motion (GtkWidget *widget, + GdkEventMotion *event) +{ + return FALSE; +} + +static gint +e_map_key_press (GtkWidget *widget, + GdkEventKey *event) +{ + EMap *map; + gboolean do_scroll; + gint xofs, yofs; + + map = E_MAP (widget); + + switch (event->keyval) + { + case GDK_KEY_Up: + do_scroll = TRUE; + xofs = 0; + yofs = -SCROLL_STEP_SIZE; + break; + + case GDK_KEY_Down: + do_scroll = TRUE; + xofs = 0; + yofs = SCROLL_STEP_SIZE; + break; + + case GDK_KEY_Left: + do_scroll = TRUE; + xofs = -SCROLL_STEP_SIZE; + yofs = 0; + break; + + case GDK_KEY_Right: + do_scroll = TRUE; + xofs = SCROLL_STEP_SIZE; + yofs = 0; + break; + + default: + return FALSE; + } + + if (do_scroll) { + gint page_size; + gint upper; + gint x, y; + + page_size = gtk_adjustment_get_page_size (map->priv->hadjustment); + upper = gtk_adjustment_get_upper (map->priv->hadjustment); + x = CLAMP (map->priv->xofs + xofs, 0, upper - page_size); + + page_size = gtk_adjustment_get_page_size (map->priv->vadjustment); + upper = gtk_adjustment_get_upper (map->priv->vadjustment); + y = CLAMP (map->priv->yofs + yofs, 0, upper - page_size); + + scroll_to (map, x, y); + + gtk_adjustment_set_value (map->priv->hadjustment, x); + gtk_adjustment_set_value (map->priv->vadjustment, y); + } + + return TRUE; +} + +static void +e_map_class_init (EMapClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EMapPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = e_map_set_property; + object_class->get_property = e_map_get_property; + object_class->finalize = e_map_finalize; + + /* Scrollable interface properties */ + g_object_class_override_property ( + object_class, PROP_HADJUSTMENT, "hadjustment"); + g_object_class_override_property ( + object_class, PROP_VADJUSTMENT, "vadjustment"); + g_object_class_override_property ( + object_class, PROP_HSCROLL_POLICY, "hscroll-policy"); + g_object_class_override_property ( + object_class, PROP_VSCROLL_POLICY, "vscroll-policy"); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->realize = e_map_realize; + widget_class->unrealize = e_map_unrealize; + widget_class->get_preferred_height = e_map_get_preferred_height; + widget_class->get_preferred_width = e_map_get_preferred_width; + widget_class->size_allocate = e_map_size_allocate; + widget_class->draw = e_map_draw; + widget_class->button_press_event = e_map_button_press; + widget_class->button_release_event = e_map_button_release; + widget_class->motion_notify_event = e_map_motion; + widget_class->key_press_event = e_map_key_press; +} + +static void +e_map_init (EMap *map) +{ + GtkWidget *widget; + gchar *map_file_name; + + map_file_name = g_build_filename ( + EVOLUTION_IMAGESDIR, "world_map-960.png", NULL); + + widget = GTK_WIDGET (map); + + map->priv = E_MAP_GET_PRIVATE (map); + + load_map_background (map, map_file_name); + g_free (map_file_name); + map->priv->frozen = FALSE; + map->priv->smooth_zoom = TRUE; + map->priv->zoom_state = E_MAP_ZOOMED_OUT; + map->priv->points = g_ptr_array_new (); + + gtk_widget_set_can_focus (widget, TRUE); + gtk_widget_set_has_window (widget, TRUE); +} + +/* ---------------- * + * Widget interface * + * ---------------- */ + +/** + * e_map_new: + * @void: + * + * Creates a new empty map widget. + * + * Return value: A newly-created map widget. + **/ + +EMap * +e_map_new (void) +{ + GtkWidget *widget; + AtkObject *a11y; + + widget = g_object_new (E_TYPE_MAP, NULL); + a11y = gtk_widget_get_accessible (widget); + atk_object_set_name (a11y, _("World Map")); + atk_object_set_role (a11y, ATK_ROLE_IMAGE); + atk_object_set_description ( + a11y, _("Mouse-based interactive map widget for selecting " + "timezone. Keyboard users should instead select the timezone " + "from the drop-down combination box below.")); + return (E_MAP (widget)); +} + +/* --- Coordinate translation --- */ + +/* These functions translate coordinates between longitude/latitude and + * the image x/y offsets, using the equidistant cylindrical projection. + * + * Longitude E <-180, 180] + * Latitude E <-90, 90] */ + +void +e_map_window_to_world (EMap *map, + gdouble win_x, + gdouble win_y, + gdouble *world_longitude, + gdouble *world_latitude) +{ + gint width, height; + + g_return_if_fail (map); + + g_return_if_fail (gtk_widget_get_realized (GTK_WIDGET (map))); + + width = E_MAP_GET_WIDTH (map); + height = E_MAP_GET_HEIGHT (map); + + *world_longitude = (win_x + map->priv->xofs - (gdouble) width / 2.0) / + ((gdouble) width / 2.0) * 180.0; + *world_latitude = ((gdouble) height / 2.0 - win_y - map->priv->yofs) / + ((gdouble) height / 2.0) * 90.0; +} + +void +e_map_world_to_window (EMap *map, + gdouble world_longitude, + gdouble world_latitude, + gdouble *win_x, + gdouble *win_y) +{ + g_return_if_fail (E_IS_MAP (map)); + g_return_if_fail (gtk_widget_get_realized (GTK_WIDGET (map))); + g_return_if_fail (world_longitude >= -180.0 && world_longitude <= 180.0); + g_return_if_fail (world_latitude >= -90.0 && world_latitude <= 90.0); + + e_map_world_to_render_surface ( + map, world_longitude, world_latitude, win_x, win_y); + + *win_x -= map->priv->xofs; + *win_y -= map->priv->yofs; +} + +/* --- Zoom --- */ + +gdouble +e_map_get_magnification (EMap *map) +{ + if (map->priv->zoom_state == E_MAP_ZOOMED_IN) return 2.0; + else return 1.0; +} + +static void +e_map_set_zoom (EMap *map, + EMapZoomState zoom) +{ + if (map->priv->zoom_state == zoom) + return; + + map->priv->zoom_state = zoom; + update_render_surface (map, TRUE); + gtk_widget_queue_draw (GTK_WIDGET (map)); +} + +void +e_map_zoom_to_location (EMap *map, + gdouble longitude, + gdouble latitude) +{ + gdouble prevlong, prevlat; + gdouble prevzoom; + + g_return_if_fail (map); + g_return_if_fail (gtk_widget_get_realized (GTK_WIDGET (map))); + + e_map_get_current_location (map, &prevlong, &prevlat); + prevzoom = e_map_get_magnification (map); + + e_map_set_zoom (map, E_MAP_ZOOMED_IN); + center_at (map, longitude, latitude); + + e_map_tween_new_from ( + map, E_MAP_TWEEN_DURATION_MSECS, + prevlong, prevlat, prevzoom); +} + +void +e_map_zoom_out (EMap *map) +{ + gdouble longitude, latitude; + gdouble prevzoom; + + g_return_if_fail (map); + g_return_if_fail (gtk_widget_get_realized (GTK_WIDGET (map))); + + e_map_get_current_location (map, &longitude, &latitude); + prevzoom = e_map_get_magnification (map); + e_map_set_zoom (map, E_MAP_ZOOMED_OUT); + center_at (map, longitude, latitude); + + e_map_tween_new_from ( + map, E_MAP_TWEEN_DURATION_MSECS, + longitude, latitude, prevzoom); +} + +void +e_map_set_smooth_zoom (EMap *map, + gboolean state) +{ + ((EMapPrivate *) map->priv)->smooth_zoom = state; +} + +gboolean +e_map_get_smooth_zoom (EMap *map) +{ + return (((EMapPrivate *) map->priv)->smooth_zoom); +} + +void +e_map_freeze (EMap *map) +{ + ((EMapPrivate *) map->priv)->frozen = TRUE; +} + +void +e_map_thaw (EMap *map) +{ + ((EMapPrivate *) map->priv)->frozen = FALSE; + update_and_paint (map); +} + +/* --- Point manipulation --- */ + +EMapPoint * +e_map_add_point (EMap *map, + gchar *name, + gdouble longitude, + gdouble latitude, + guint32 color_rgba) +{ + EMapPoint *point; + + point = g_new0 (EMapPoint, 1); + + point->name = name; /* Can be NULL */ + point->longitude = longitude; + point->latitude = latitude; + point->rgba = color_rgba; + + g_ptr_array_add (map->priv->points, (gpointer) point); + + if (!map->priv->frozen) + { + update_render_point (map, point); + repaint_point (map, point); + } + + return point; +} + +void +e_map_remove_point (EMap *map, + EMapPoint *point) +{ + g_ptr_array_remove (map->priv->points, point); + + if (!((EMapPrivate *) map->priv)->frozen) + { + /* FIXME: Re-scaling the whole pixbuf is more than a little + * overkill when just one point is removed */ + + update_render_surface (map, TRUE); + repaint_point (map, point); + } + + g_free (point); +} + +void +e_map_point_get_location (EMapPoint *point, + gdouble *longitude, + gdouble *latitude) +{ + *longitude = point->longitude; + *latitude = point->latitude; +} + +gchar * +e_map_point_get_name (EMapPoint *point) +{ + return point->name; +} + +guint32 +e_map_point_get_color_rgba (EMapPoint *point) +{ + return point->rgba; +} + +void +e_map_point_set_color_rgba (EMap *map, + EMapPoint *point, + guint32 color_rgba) +{ + point->rgba = color_rgba; + + if (!((EMapPrivate *) map->priv)->frozen) + { + /* TODO: Redraw area around point only */ + + update_render_point (map, point); + repaint_point (map, point); + } +} + +void +e_map_point_set_data (EMapPoint *point, + gpointer data) +{ + point->user_data = data; +} + +gpointer +e_map_point_get_data (EMapPoint *point) +{ + return point->user_data; +} + +gboolean +e_map_point_is_in_view (EMap *map, + EMapPoint *point) +{ + GtkAllocation allocation; + gdouble x, y; + + if (!map->priv->map_render_surface) return FALSE; + + e_map_world_to_window (map, point->longitude, point->latitude, &x, &y); + gtk_widget_get_allocation (GTK_WIDGET (map), &allocation); + + if (x >= 0 && x < allocation.width && + y >= 0 && y < allocation.height) + return TRUE; + + return FALSE; +} + +EMapPoint * +e_map_get_closest_point (EMap *map, + gdouble longitude, + gdouble latitude, + gboolean in_view) +{ + EMapPoint *point_chosen = NULL, *point; + gdouble min_dist = 0.0, dist; + gdouble dx, dy; + gint i; + + for (i = 0; i < map->priv->points->len; i++) + { + point = g_ptr_array_index (map->priv->points, i); + if (in_view && !e_map_point_is_in_view (map, point)) continue; + + dx = point->longitude - longitude; + dy = point->latitude - latitude; + dist = dx * dx + dy * dy; + + if (!point_chosen || dist < min_dist) + { + min_dist = dist; + point_chosen = point; + } + } + + return point_chosen; +} + +/* ------------------ * + * Internal functions * + * ------------------ */ + +static void +update_and_paint (EMap *map) +{ + update_render_surface (map, TRUE); + gtk_widget_queue_draw (GTK_WIDGET (map)); +} + +static gint +load_map_background (EMap *map, + gchar *name) +{ + GdkPixbuf *pb0; + + pb0 = gdk_pixbuf_new_from_file (name, NULL); + if (!pb0) + return FALSE; + + if (map->priv->map_pixbuf) g_object_unref (map->priv->map_pixbuf); + map->priv->map_pixbuf = pb0; + update_render_surface (map, TRUE); + + return TRUE; +} + +static void +update_render_surface (EMap *map, + gboolean render_overlays) +{ + EMapPoint *point; + GtkAllocation allocation; + gint width, height, orig_width, orig_height; + gdouble zoom; + gint i; + + if (!gtk_widget_get_realized (GTK_WIDGET (map))) + return; + + gtk_widget_get_allocation (GTK_WIDGET (map), &allocation); + + /* Set up value shortcuts */ + + width = allocation.width; + height = allocation.height; + orig_width = gdk_pixbuf_get_width (map->priv->map_pixbuf); + orig_height = gdk_pixbuf_get_height (map->priv->map_pixbuf); + + /* Compute scaled width and height based on the extreme dimension */ + + if ((gdouble) width / orig_width > (gdouble) height / orig_height) + zoom = (gdouble) width / (gdouble) orig_width; + else + zoom = (gdouble) height / (gdouble) orig_height; + + if (map->priv->zoom_state == E_MAP_ZOOMED_IN) + zoom *= 2.0; + height = (orig_height * zoom) + 0.5; + width = (orig_width * zoom) + 0.5; + + /* Reallocate the pixbuf */ + + if (map->priv->map_render_surface) + cairo_surface_destroy (map->priv->map_render_surface); + map->priv->map_render_surface = gdk_window_create_similar_surface ( + gtk_widget_get_window (GTK_WIDGET (map)), + CAIRO_CONTENT_COLOR, width, height); + + /* Scale the original map into the rendering pixbuf */ + + if (width > 1 && height > 1) { + cairo_t *cr = cairo_create (map->priv->map_render_surface); + cairo_scale ( + cr, + (gdouble) width / orig_width, + (gdouble) height / orig_height); + gdk_cairo_set_source_pixbuf (cr, map->priv->map_pixbuf, 0, 0); + cairo_paint (cr); + cairo_destroy (cr); + } + + /* Compute image offsets with respect to window */ + + set_scroll_area (map, width, height); + + if (render_overlays) { + /* Add points */ + + for (i = 0; i < map->priv->points->len; i++) { + point = g_ptr_array_index (map->priv->points, i); + update_render_point (map, point); + } + } +} + +/* Redraw point in client surface */ + +static void +update_render_point (EMap *map, + EMapPoint *point) +{ + cairo_t *cr; + gdouble px, py; + static guchar mask1[] = { 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 }; + static guchar mask2[] = { 0x00, 0xff, 0x00, 0x00, + 0xff, 0xff, 0xff, 0x00, + 0x00, 0xff, 0x00, 0x00 }; + cairo_surface_t *mask; + + if (map->priv->map_render_surface == NULL) + return; + + cr = cairo_create (map->priv->map_render_surface); + cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + + e_map_world_to_window (map, point->longitude, point->latitude, &px, &py); + px = floor (px + map->priv->xofs); + py = floor (py + map->priv->yofs); + + cairo_set_source_rgb (cr, 0, 0, 0); + mask = cairo_image_surface_create_for_data (mask1, CAIRO_FORMAT_A8, 5, 5, 8); + cairo_mask_surface (cr, mask, px - 2, py - 2); + cairo_surface_destroy (mask); + + cairo_set_source_rgba ( + cr, + ((point->rgba >> 24) & 0xff) / 255.0, + ((point->rgba >> 16) & 0xff) / 255.0, + ((point->rgba >> 8) & 0xff) / 255.0, + ( point->rgba & 0xff) / 255.0); + mask = cairo_image_surface_create_for_data (mask2, CAIRO_FORMAT_A8, 3, 3, 4); + cairo_mask_surface (cr, mask, px - 1, py - 1); + cairo_surface_destroy (mask); + + cairo_destroy (cr); +} + +/* Repaint point on X server */ + +static void +repaint_point (EMap *map, + EMapPoint *point) +{ + gdouble px, py; + + if (!gtk_widget_is_drawable (GTK_WIDGET (map))) + return; + + e_map_world_to_window (map, point->longitude, point->latitude, &px, &py); + + gtk_widget_queue_draw_area ( + GTK_WIDGET (map), + (gint) px - 2, (gint) py - 2, + 5, 5); +} + +static void +center_at (EMap *map, + gdouble longitude, + gdouble latitude) +{ + GtkAllocation allocation; + gint pb_width, pb_height; + gdouble x, y; + + e_map_world_to_render_surface (map, longitude, latitude, &x, &y); + + pb_width = E_MAP_GET_WIDTH (map); + pb_height = E_MAP_GET_HEIGHT (map); + + gtk_widget_get_allocation (GTK_WIDGET (map), &allocation); + + x = CLAMP (x - (allocation.width / 2), 0, pb_width - allocation.width); + y = CLAMP (y - (allocation.height / 2), 0, pb_height - allocation.height); + + gtk_adjustment_set_value (map->priv->hadjustment, x); + gtk_adjustment_set_value (map->priv->vadjustment, y); + + gtk_widget_queue_draw (GTK_WIDGET (map)); +} + +/* Scrolls the view to the specified offsets. Does not perform range checking! */ + +static void +scroll_to (EMap *map, + gint x, + gint y) +{ + gint xofs, yofs; + + /* Compute offsets and check bounds */ + + xofs = x - map->priv->xofs; + yofs = y - map->priv->yofs; + + if (xofs == 0 && yofs == 0) + return; + + map->priv->xofs = x; + map->priv->yofs = y; + + gtk_widget_queue_draw (GTK_WIDGET (map)); +} + +static void +set_scroll_area (EMap *view, + gint width, + gint height) +{ + EMapPrivate *priv; + GtkAllocation allocation; + + priv = view->priv; + + if (!gtk_widget_get_realized (GTK_WIDGET (view))) + return; + + if (!priv->hadjustment || !priv->vadjustment) + return; + + g_object_freeze_notify (G_OBJECT (priv->hadjustment)); + g_object_freeze_notify (G_OBJECT (priv->vadjustment)); + + gtk_widget_get_allocation (GTK_WIDGET (view), &allocation); + + priv->xofs = CLAMP (priv->xofs, 0, width - allocation.width); + priv->yofs = CLAMP (priv->yofs, 0, height - allocation.height); + + gtk_adjustment_configure ( + priv->hadjustment, + priv->xofs, + 0, width, + SCROLL_STEP_SIZE, + allocation.width / 2, + allocation.width); + gtk_adjustment_configure ( + priv->vadjustment, + priv->yofs, + 0, height, + SCROLL_STEP_SIZE, + allocation.height / 2, + allocation.height); + + g_object_thaw_notify (G_OBJECT (priv->hadjustment)); + g_object_thaw_notify (G_OBJECT (priv->vadjustment)); +} diff --git a/e-util/e-map.h b/e-util/e-map.h new file mode 100644 index 0000000000..cb2923d3fb --- /dev/null +++ b/e-util/e-map.h @@ -0,0 +1,155 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Hans Petter Jansson <hpj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MAP_H +#define E_MAP_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_MAP \ + (e_map_get_type ()) +#define E_MAP(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MAP, EMap)) +#define E_MAP_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MAP, EMapClass)) +#define E_IS_MAP(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MAP)) +#define E_IS_MAP_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MAP)) +#define E_MAP_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MAP, EMapClass)) + +G_BEGIN_DECLS + +typedef struct _EMap EMap; +typedef struct _EMapClass EMapClass; +typedef struct _EMapPrivate EMapPrivate; +typedef struct _EMapPoint EMapPoint; + +struct _EMap { + GtkWidget widget; + EMapPrivate *priv; +}; + +struct _EMapClass { + GtkWidgetClass parent_class; + + /* Notification signals */ + void (*zoom_fit) (EMap * view); + + /* GTK+ scrolling interface */ + void (*set_scroll_adjustments) (GtkWidget * widget, + GtkAdjustment * hadj, + GtkAdjustment * vadj); +}; + +/* The definition of Dot */ + +struct _EMapPoint { + gchar *name; /* Can be NULL */ + gdouble longitude, latitude; + guint32 rgba; + gpointer user_data; +}; + +/* --- Widget --- */ + +GType e_map_get_type (void); + +EMap *e_map_new (void); + +/* Stop doing redraws when map data changes (e.g. by modifying points) */ +void e_map_freeze (EMap *map); + +/* Do an immediate repaint, and start doing realtime repaints again */ +void e_map_thaw (EMap *map); + +/* --- Coordinate translation --- */ + +/* Translates window-relative coords to lat/long */ +void e_map_window_to_world (EMap *map, + gdouble win_x, gdouble win_y, + gdouble *world_longitude, gdouble *world_latitude); + +/* Translates lat/long to window-relative coordinates. Note that the + * returned coordinates can be negative or greater than the current size + * of the allocation area */ +void e_map_world_to_window (EMap *map, + gdouble world_longitude, gdouble world_latitude, + gdouble *win_x, gdouble *win_y); + +/* --- Zoom --- */ + +gdouble e_map_get_magnification (EMap *map); + +/* Pass TRUE if we want the smooth zoom hack */ +void e_map_set_smooth_zoom (EMap *map, gboolean state); + +/* TRUE if smooth zoom hack will be employed */ +gboolean e_map_get_smooth_zoom (EMap *map); + +/* NB: Function definition will change shortly */ +void e_map_zoom_to_location (EMap *map, gdouble longitude, gdouble latitude); + +/* Zoom to mag factor 1.0 */ +void e_map_zoom_out (EMap *map); + +/* --- Points --- */ + +EMapPoint *e_map_add_point (EMap *map, gchar *name, + gdouble longitude, gdouble latitude, + guint32 color_rgba); + +void e_map_remove_point (EMap *map, EMapPoint *point); + +void e_map_point_get_location (EMapPoint *point, + gdouble *longitude, gdouble *latitude); + +gchar *e_map_point_get_name (EMapPoint *point); + +guint32 e_map_point_get_color_rgba (EMapPoint *point); + +void e_map_point_set_color_rgba (EMap *map, EMapPoint *point, guint32 color_rgba); + +void e_map_point_set_data (EMapPoint *point, gpointer data); + +gpointer e_map_point_get_data (EMapPoint *point); + +gboolean e_map_point_is_in_view (EMap *map, EMapPoint *point); + +EMapPoint *e_map_get_closest_point (EMap *map, gdouble longitude, gdouble latitude, + gboolean in_view); + +G_END_DECLS + +#endif diff --git a/e-util/e-menu-tool-action.c b/e-util/e-menu-tool-action.c new file mode 100644 index 0000000000..3ed37cb008 --- /dev/null +++ b/e-util/e-menu-tool-action.c @@ -0,0 +1,59 @@ +/* + * e-menu-tool-action.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-menu-tool-action.h" + +G_DEFINE_TYPE ( + EMenuToolAction, + e_menu_tool_action, + GTK_TYPE_ACTION) + +static void +e_menu_tool_action_class_init (EMenuToolActionClass *class) +{ + GtkActionClass *action_class; + + action_class = GTK_ACTION_CLASS (class); + action_class->toolbar_item_type = GTK_TYPE_MENU_TOOL_BUTTON; +} + +static void +e_menu_tool_action_init (EMenuToolAction *action) +{ +} + +EMenuToolAction * +e_menu_tool_action_new (const gchar *name, + const gchar *label, + const gchar *tooltip, + const gchar *stock_id) +{ + g_return_val_if_fail (name != NULL, NULL); + + return g_object_new ( + E_TYPE_MENU_TOOL_ACTION, + "name", name, "label", label, "tooltip", + tooltip, "stock-id", stock_id, NULL); +} diff --git a/e-util/e-menu-tool-action.h b/e-util/e-menu-tool-action.h new file mode 100644 index 0000000000..aee47686e2 --- /dev/null +++ b/e-util/e-menu-tool-action.h @@ -0,0 +1,75 @@ +/* + * e-menu-tool-action.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* This is a trivial GtkAction subclass that sets the toolbar + * item type to GtkMenuToolButton instead of GtkToolButton. */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MENU_TOOL_ACTION_H +#define E_MENU_TOOL_ACTION_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_MENU_TOOL_ACTION \ + (e_menu_tool_action_get_type ()) +#define E_MENU_TOOL_ACTION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MENU_TOOL_ACTION, EMenuToolAction)) +#define E_MENU_TOOL_ACTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MENU_TOOL_ACTION, EMenuToolActionClass)) +#define E_IS_MENU_TOOL_ACTION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MENU_TOOL_ACTION)) +#define E_IS_MENU_TOOL_ACTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MENU_TOOL_ACTION)) +#define E_MENU_TOOL_ACTION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MENU_TOOL_ACTION, EMenuToolActionClass)) + +G_BEGIN_DECLS + +typedef struct _EMenuToolAction EMenuToolAction; +typedef struct _EMenuToolActionClass EMenuToolActionClass; + +struct _EMenuToolAction { + GtkAction parent; +}; + +struct _EMenuToolActionClass { + GtkActionClass parent_class; +}; + +GType e_menu_tool_action_get_type (void); +EMenuToolAction * + e_menu_tool_action_new (const gchar *name, + const gchar *label, + const gchar *tooltip, + const gchar *stock_id); + +G_END_DECLS + +#endif /* E_MENU_TOOL_ACTION_H */ diff --git a/e-util/e-menu-tool-button.c b/e-util/e-menu-tool-button.c new file mode 100644 index 0000000000..c2a68d0c05 --- /dev/null +++ b/e-util/e-menu-tool-button.c @@ -0,0 +1,273 @@ +/* + * e-menu-tool-button.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-menu-tool-button.h" + +#define E_MENU_TOOL_BUTTON_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_MENU_TOOL_BUTTON, EMenuToolButtonPrivate)) + +struct _EMenuToolButtonPrivate { + gchar *prefer_item; +}; + +enum { + PROP_0, + PROP_PREFER_ITEM +}; + +G_DEFINE_TYPE ( + EMenuToolButton, + e_menu_tool_button, + GTK_TYPE_MENU_TOOL_BUTTON) + +static GtkWidget * +menu_tool_button_clone_image (GtkWidget *source) +{ + GtkIconSize size; + GtkImageType image_type; + const gchar *icon_name; + + /* XXX This isn't general purpose because it requires that the + * source image be using a named icon. Somewhat surprised + * GTK+ doesn't offer something like this. */ + image_type = gtk_image_get_storage_type (GTK_IMAGE (source)); + g_return_val_if_fail (image_type == GTK_IMAGE_ICON_NAME, NULL); + gtk_image_get_icon_name (GTK_IMAGE (source), &icon_name, &size); + + return gtk_image_new_from_icon_name (icon_name, size); +} + +static GtkMenuItem * +menu_tool_button_get_prefer_menu_item (GtkMenuToolButton *menu_tool_button) +{ + GtkWidget *menu; + GtkMenuItem *item = NULL; + GList *children; + const gchar *prefer_item; + + menu = gtk_menu_tool_button_get_menu (menu_tool_button); + if (!GTK_IS_MENU (menu)) + return NULL; + + children = gtk_container_get_children (GTK_CONTAINER (menu)); + if (children == NULL) + return NULL; + + prefer_item = e_menu_tool_button_get_prefer_item ( + E_MENU_TOOL_BUTTON (menu_tool_button)); + if (prefer_item != NULL && *prefer_item != '\0') { + GtkAction *action; + GList *iter; + + for (iter = children; iter != NULL; iter = iter->next) { + item = GTK_MENU_ITEM (iter->data); + + if (!item) + continue; + + action = gtk_activatable_get_related_action ( + GTK_ACTIVATABLE (item)); + if (action && g_strcmp0 (gtk_action_get_name (action), prefer_item) == 0) + break; + else if (!action && g_strcmp0 (gtk_widget_get_name (GTK_WIDGET (item)), prefer_item) == 0) + break; + + item = NULL; + } + } + + if (!item) + item = GTK_MENU_ITEM (children->data); + + g_list_free (children); + + return item; +} + +static void +menu_tool_button_update_button (GtkToolButton *tool_button) +{ + GtkMenuItem *menu_item; + GtkMenuToolButton *menu_tool_button; + GtkImageMenuItem *image_menu_item; + GtkAction *action; + GtkWidget *image; + gchar *tooltip = NULL; + + menu_tool_button = GTK_MENU_TOOL_BUTTON (tool_button); + menu_item = menu_tool_button_get_prefer_menu_item (menu_tool_button); + if (!GTK_IS_IMAGE_MENU_ITEM (menu_item)) + return; + + image_menu_item = GTK_IMAGE_MENU_ITEM (menu_item); + image = gtk_image_menu_item_get_image (image_menu_item); + if (!GTK_IS_IMAGE (image)) + return; + + image = menu_tool_button_clone_image (image); + gtk_tool_button_set_icon_widget (tool_button, image); + gtk_widget_show (image); + + /* If the menu item is a proxy for a GtkAction, extract + * the action's tooltip and use it as our own tooltip. */ + action = gtk_activatable_get_related_action ( + GTK_ACTIVATABLE (menu_item)); + if (action != NULL) + g_object_get (action, "tooltip", &tooltip, NULL); + gtk_widget_set_tooltip_text (GTK_WIDGET (tool_button), tooltip); + g_free (tooltip); +} + +static void +menu_tool_button_clicked (GtkToolButton *tool_button) +{ + GtkMenuItem *menu_item; + GtkMenuToolButton *menu_tool_button; + + menu_tool_button = GTK_MENU_TOOL_BUTTON (tool_button); + menu_item = menu_tool_button_get_prefer_menu_item (menu_tool_button); + + if (GTK_IS_MENU_ITEM (menu_item)) + gtk_menu_item_activate (menu_item); +} + +static void +menu_tool_button_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_PREFER_ITEM: + e_menu_tool_button_set_prefer_item ( + E_MENU_TOOL_BUTTON (object), + g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +menu_tool_button_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_PREFER_ITEM: + g_value_set_string ( + value, e_menu_tool_button_get_prefer_item ( + E_MENU_TOOL_BUTTON (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +menu_tool_button_dispose (GObject *object) +{ + EMenuToolButtonPrivate *priv = E_MENU_TOOL_BUTTON (object)->priv; + + if (priv->prefer_item) { + g_free (priv->prefer_item); + priv->prefer_item = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_menu_tool_button_parent_class)->dispose (object); +} + +static void +e_menu_tool_button_class_init (EMenuToolButtonClass *class) +{ + GObjectClass *object_class; + GtkToolButtonClass *tool_button_class; + + g_type_class_add_private (class, sizeof (EMenuToolButtonPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = menu_tool_button_set_property; + object_class->get_property = menu_tool_button_get_property; + object_class->dispose = menu_tool_button_dispose; + + tool_button_class = GTK_TOOL_BUTTON_CLASS (class); + tool_button_class->clicked = menu_tool_button_clicked; + + g_object_class_install_property ( + object_class, + PROP_PREFER_ITEM, + g_param_spec_string ( + "prefer-item", + "Prefer Item", + "Name of an item to show instead of the first", + NULL, + G_PARAM_READWRITE)); +} + +static void +e_menu_tool_button_init (EMenuToolButton *button) +{ + button->priv = E_MENU_TOOL_BUTTON_GET_PRIVATE (button); + + button->priv->prefer_item = NULL; + + g_signal_connect ( + button, "notify::menu", + G_CALLBACK (menu_tool_button_update_button), NULL); +} + +GtkToolItem * +e_menu_tool_button_new (const gchar *label) +{ + return g_object_new (E_TYPE_MENU_TOOL_BUTTON, "label", label, NULL); +} + +void +e_menu_tool_button_set_prefer_item (EMenuToolButton *button, + const gchar *prefer_item) +{ + g_return_if_fail (button != NULL); + g_return_if_fail (E_IS_MENU_TOOL_BUTTON (button)); + + if (g_strcmp0 (button->priv->prefer_item, prefer_item) == 0) + return; + + g_free (button->priv->prefer_item); + button->priv->prefer_item = g_strdup (prefer_item); + + g_object_notify (G_OBJECT (button), "prefer-item"); +} + +const gchar * +e_menu_tool_button_get_prefer_item (EMenuToolButton *button) +{ + g_return_val_if_fail (button != NULL, NULL); + g_return_val_if_fail (E_IS_MENU_TOOL_BUTTON (button), NULL); + + return button->priv->prefer_item; +} diff --git a/e-util/e-menu-tool-button.h b/e-util/e-menu-tool-button.h new file mode 100644 index 0000000000..04519958d2 --- /dev/null +++ b/e-util/e-menu-tool-button.h @@ -0,0 +1,77 @@ +/* + * e-menu-tool-button.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* EMenuToolButton is a variation of GtkMenuToolButton where the + * button icon always reflects the first menu item, and clicking + * the button activates the first menu item. */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MENU_TOOL_BUTTON_H +#define E_MENU_TOOL_BUTTON_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_MENU_TOOL_BUTTON \ + (e_menu_tool_button_get_type ()) +#define E_MENU_TOOL_BUTTON(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MENU_TOOL_BUTTON, EMenuToolButton)) +#define E_MENU_TOOL_BUTTON_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MENU_TOOL_BUTTON, EMenuToolButtonClass)) +#define E_IS_MENU_TOOL_BUTTON(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MENU_TOOL_BUTTON)) +#define E_IS_MENU_TOOL_BUTTON_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MENU_TOOL_BUTTON)) +#define E_MENU_TOOL_BUTTON_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MENU_TOOL_BUTTON, EMenuToolButtonClass)) + +G_BEGIN_DECLS + +typedef struct _EMenuToolButton EMenuToolButton; +typedef struct _EMenuToolButtonPrivate EMenuToolButtonPrivate; +typedef struct _EMenuToolButtonClass EMenuToolButtonClass; + +struct _EMenuToolButton { + GtkMenuToolButton parent; + EMenuToolButtonPrivate *priv; +}; + +struct _EMenuToolButtonClass { + GtkMenuToolButtonClass parent_class; +}; + +GType e_menu_tool_button_get_type (void); +GtkToolItem * e_menu_tool_button_new (const gchar *label); +void e_menu_tool_button_set_prefer_item (EMenuToolButton *button, + const gchar *prefer_item); +const gchar * e_menu_tool_button_get_prefer_item (EMenuToolButton *button); + +G_END_DECLS + +#endif /* E_MENU_TOOL_BUTTON_H */ diff --git a/e-util/e-util.c b/e-util/e-misc-utils.c index 8d47da2186..7d1a0c6028 100644 --- a/e-util/e-util.c +++ b/e-util/e-misc-utils.c @@ -20,15 +20,12 @@ * */ -/** - * SECTION: e-util - * @include: e-util/e-util.h - **/ - #ifdef HAVE_CONFIG_H #include <config.h> #endif +#include "e-misc-utils.h" + #include <stdlib.h> #include <stdio.h> #include <errno.h> @@ -53,9 +50,7 @@ #include <camel/camel.h> #include <libedataserver/libedataserver.h> -#include "filter/e-filter-option.h" - -#include "e-util.h" +#include "e-filter-option.h" #include "e-util-private.h" typedef struct _WindowData WindowData; @@ -648,6 +643,73 @@ e_action_group_add_actions_localized (GtkActionGroup *action_group, g_object_unref (tmp_group); } +/** + * e_builder_get_widget: + * @builder: a #GtkBuilder + * @widget_name: name of a widget in @builder + * + * Gets the widget named @widget_name. Note that this function does not + * increment the reference count of the returned widget. If @widget_name + * could not be found in the @builder<!-- -->'s object tree, a run-time + * warning is emitted since this usually indicates a programming error. + * + * This is a convenience function to work around the awkwardness of + * #GtkBuilder returning #GObject pointers, when the vast majority of + * the time you want a #GtkWidget pointer. + * + * If you need something from @builder other than a #GtkWidget, or you + * want to test for the existence of some widget name without incurring + * a run-time warning, use gtk_builder_get_object(). + * + * Returns: the widget named @widget_name, or %NULL + **/ +GtkWidget * +e_builder_get_widget (GtkBuilder *builder, + const gchar *widget_name) +{ + GObject *object; + + g_return_val_if_fail (GTK_IS_BUILDER (builder), NULL); + g_return_val_if_fail (widget_name != NULL, NULL); + + object = gtk_builder_get_object (builder, widget_name); + if (object == NULL) { + g_warning ("Could not find widget '%s'", widget_name); + return NULL; + } + + return GTK_WIDGET (object); +} + +/** + * e_load_ui_builder_definition: + * @builder: a #GtkBuilder + * @basename: basename of the UI definition file + * + * Loads a UI definition into @builder from Evolution's UI directory. + * Failure here is fatal, since the application can't function without + * its UI definitions. + **/ +void +e_load_ui_builder_definition (GtkBuilder *builder, + const gchar *basename) +{ + gchar *filename; + GError *error = NULL; + + g_return_if_fail (GTK_IS_BUILDER (builder)); + g_return_if_fail (basename != NULL); + + filename = g_build_filename (EVOLUTION_UIDIR, basename, NULL); + gtk_builder_add_from_file (builder, filename, &error); + g_free (filename); + + if (error != NULL) { + g_error ("%s: %s", basename, error->message); + g_assert_not_reached (); + } +} + /* Helper for e_categories_add_change_hook() */ static void categories_changed_cb (GObject *useless_opaque_object, @@ -719,6 +781,233 @@ e_categories_add_change_hook (GHookFunc func, } /** + * e_flexible_strtod: + * @nptr: the string to convert to a numeric value. + * @endptr: if non-NULL, it returns the character after + * the last character used in the conversion. + * + * Converts a string to a gdouble value. This function detects + * strings either in the standard C locale or in the current locale. + * + * This function is typically used when reading configuration files or + * other non-user input that should not be locale dependent, but may + * have been in the past. To handle input from the user you should + * normally use the locale-sensitive system strtod function. + * + * To convert from a double to a string in a locale-insensitive way, use + * @g_ascii_dtostr. + * + * Returns: the gdouble value + **/ +gdouble +e_flexible_strtod (const gchar *nptr, + gchar **endptr) +{ + gchar *fail_pos; + gdouble val; + struct lconv *locale_data; + const gchar *decimal_point; + gint decimal_point_len; + const gchar *p, *decimal_point_pos; + const gchar *end = NULL; /* Silence gcc */ + gchar *copy, *c; + + g_return_val_if_fail (nptr != NULL, 0); + + fail_pos = NULL; + + locale_data = localeconv (); + decimal_point = locale_data->decimal_point; + decimal_point_len = strlen (decimal_point); + + g_return_val_if_fail (decimal_point_len != 0, 0); + + decimal_point_pos = NULL; + if (!strcmp (decimal_point, ".")) + return strtod (nptr, endptr); + + p = nptr; + + /* Skip leading space */ + while (isspace ((guchar) * p)) + p++; + + /* Skip leading optional sign */ + if (*p == '+' || *p == '-') + p++; + + if (p[0] == '0' && + (p[1] == 'x' || p[1] == 'X')) { + p += 2; + /* HEX - find the (optional) decimal point */ + + while (isxdigit ((guchar) * p)) + p++; + + if (*p == '.') { + decimal_point_pos = p++; + + while (isxdigit ((guchar) * p)) + p++; + + if (*p == 'p' || *p == 'P') + p++; + if (*p == '+' || *p == '-') + p++; + while (isdigit ((guchar) * p)) + p++; + end = p; + } else if (strncmp (p, decimal_point, decimal_point_len) == 0) { + return strtod (nptr, endptr); + } + } else { + while (isdigit ((guchar) * p)) + p++; + + if (*p == '.') { + decimal_point_pos = p++; + + while (isdigit ((guchar) * p)) + p++; + + if (*p == 'e' || *p == 'E') + p++; + if (*p == '+' || *p == '-') + p++; + while (isdigit ((guchar) * p)) + p++; + end = p; + } else if (strncmp (p, decimal_point, decimal_point_len) == 0) { + return strtod (nptr, endptr); + } + } + /* For the other cases, we need not convert the decimal point */ + + if (!decimal_point_pos) + return strtod (nptr, endptr); + + /* We need to convert the '.' to the locale specific decimal point */ + copy = g_malloc (end - nptr + 1 + decimal_point_len); + + c = copy; + memcpy (c, nptr, decimal_point_pos - nptr); + c += decimal_point_pos - nptr; + memcpy (c, decimal_point, decimal_point_len); + c += decimal_point_len; + memcpy (c, decimal_point_pos + 1, end - (decimal_point_pos + 1)); + c += end - (decimal_point_pos + 1); + *c = 0; + + val = strtod (copy, &fail_pos); + + if (fail_pos) { + if (fail_pos > decimal_point_pos) + fail_pos = + (gchar *) nptr + (fail_pos - copy) - + (decimal_point_len - 1); + else + fail_pos = (gchar *) nptr + (fail_pos - copy); + } + + g_free (copy); + + if (endptr) + *endptr = fail_pos; + + return val; +} + +/** + * e_ascii_dtostr: + * @buffer: A buffer to place the resulting string in + * @buf_len: The length of the buffer. + * @format: The printf-style format to use for the + * code to use for converting. + * @d: The double to convert + * + * Converts a double to a string, using the '.' as + * decimal_point. To format the number you pass in + * a printf-style formating string. Allowed conversion + * specifiers are eEfFgG. + * + * If you want to generates enough precision that converting + * the string back using @g_strtod gives the same machine-number + * (on machines with IEEE compatible 64bit doubles) use the format + * string "%.17g". If you do this it is guaranteed that the size + * of the resulting string will never be larger than + * @G_ASCII_DTOSTR_BUF_SIZE bytes. + * + * Returns: the pointer to the buffer with the converted string + **/ +gchar * +e_ascii_dtostr (gchar *buffer, + gint buf_len, + const gchar *format, + gdouble d) +{ + struct lconv *locale_data; + const gchar *decimal_point; + gint decimal_point_len; + gchar *p; + gint rest_len; + gchar format_char; + + g_return_val_if_fail (buffer != NULL, NULL); + g_return_val_if_fail (format[0] == '%', NULL); + g_return_val_if_fail (strpbrk (format + 1, "'l%") == NULL, NULL); + + format_char = format[strlen (format) - 1]; + + g_return_val_if_fail (format_char == 'e' || format_char == 'E' || + format_char == 'f' || format_char == 'F' || + format_char == 'g' || format_char == 'G', + NULL); + + if (format[0] != '%') + return NULL; + + if (strpbrk (format + 1, "'l%")) + return NULL; + + if (!(format_char == 'e' || format_char == 'E' || + format_char == 'f' || format_char == 'F' || + format_char == 'g' || format_char == 'G')) + return NULL; + + g_snprintf (buffer, buf_len, format, d); + + locale_data = localeconv (); + decimal_point = locale_data->decimal_point; + decimal_point_len = strlen (decimal_point); + + g_return_val_if_fail (decimal_point_len != 0, NULL); + + if (strcmp (decimal_point, ".")) { + p = buffer; + + if (*p == '+' || *p == '-') + p++; + + while (isdigit ((guchar) * p)) + p++; + + if (strncmp (p, decimal_point, decimal_point_len) == 0) { + *p = '.'; + p++; + if (decimal_point_len > 1) { + rest_len = strlen (p + (decimal_point_len - 1)); + memmove ( + p, p + (decimal_point_len - 1), + rest_len); + p[rest_len] = 0; + } + } + } + + return buffer; +} + +/** * e_str_without_underscores: * @string: the string to strip underscores from * @@ -1318,7 +1607,6 @@ e_util_guess_mime_type (const gchar *filename, return mime_type; } -/* XXX: Should e-util/ really depend on filter/ ?? */ GSList * e_util_get_category_filter_options (void) { diff --git a/e-util/e-misc-utils.h b/e-util/e-misc-utils.h new file mode 100644 index 0000000000..0bd465e7d1 --- /dev/null +++ b/e-util/e-misc-utils.h @@ -0,0 +1,175 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_MISC_UTILS_H +#define E_MISC_UTILS_H + +#include <sys/types.h> +#include <gtk/gtk.h> +#include <limits.h> + +#include <libedataserver/libedataserver.h> + +#include "e-marshal.h" +#include "e-util-enums.h" + +G_BEGIN_DECLS + +typedef enum { + E_FOCUS_NONE, + E_FOCUS_CURRENT, + E_FOCUS_START, + E_FOCUS_END +} EFocus; + +typedef enum { + E_RESTORE_WINDOW_SIZE = 1 << 0, + E_RESTORE_WINDOW_POSITION = 1 << 1 +} ERestoreWindowFlags; + +const gchar * e_get_accels_filename (void); +void e_show_uri (GtkWindow *parent, + const gchar *uri); +void e_display_help (GtkWindow *parent, + const gchar *link_id); +void e_restore_window (GtkWindow *window, + const gchar *settings_path, + ERestoreWindowFlags flags); +GtkAction * e_lookup_action (GtkUIManager *ui_manager, + const gchar *action_name); +GtkActionGroup *e_lookup_action_group (GtkUIManager *ui_manager, + const gchar *group_name); +gint e_action_compare_by_label (GtkAction *action1, + GtkAction *action2); +void e_action_group_remove_all_actions + (GtkActionGroup *action_group); +GtkRadioAction *e_radio_action_get_current_action + (GtkRadioAction *radio_action); +void e_action_group_add_actions_localized + (GtkActionGroup *action_group, + const gchar *translation_domain, + const GtkActionEntry *entries, + guint n_entries, + gpointer user_data); +GtkWidget * e_builder_get_widget (GtkBuilder *builder, + const gchar *widget_name); +void e_load_ui_builder_definition (GtkBuilder *builder, + const gchar *basename); +void e_categories_add_change_hook (GHookFunc func, + gpointer object); + +/* String to/from double conversion functions */ +gdouble e_flexible_strtod (const gchar *nptr, + gchar **endptr); + +/* 29 bytes should enough for all possible values that + * g_ascii_dtostr can produce with the %.17g format. + * Then add 10 for good measure */ +#define E_ASCII_DTOSTR_BUF_SIZE (DBL_DIG + 12 + 10) +gchar * e_ascii_dtostr (gchar *buffer, + gint buf_len, + const gchar *format, + gdouble d); + +gchar * e_str_without_underscores (const gchar *string); +gint e_str_compare (gconstpointer x, + gconstpointer y); +gint e_str_case_compare (gconstpointer x, + gconstpointer y); +gint e_collate_compare (gconstpointer x, + gconstpointer y); +gint e_int_compare (gconstpointer x, + gconstpointer y); +guint32 e_color_to_value (GdkColor *color); + +guint32 e_rgba_to_value (GdkRGBA *rgba); + +/* This only makes a filename safe for usage as a filename. + * It still may have shell meta-characters in it. */ +gchar * e_format_number (gint number); + +typedef gint (*ESortCompareFunc) (gconstpointer first, + gconstpointer second, + gpointer closure); + +void e_bsearch (gconstpointer key, + gconstpointer base, + gsize nmemb, + gsize size, + ESortCompareFunc compare, + gpointer closure, + gsize *start, + gsize *end); + +gsize e_strftime_fix_am_pm (gchar *str, + gsize max, + const gchar *fmt, + const struct tm *tm); +gsize e_utf8_strftime_fix_am_pm (gchar *str, + gsize max, + const gchar *fmt, + const struct tm *tm); +const gchar * e_get_month_name (GDateMonth month, + gboolean abbreviated); +const gchar * e_get_weekday_name (GDateWeekday weekday, + gboolean abbreviated); + +gboolean e_file_lock_create (void); +void e_file_lock_destroy (void); +gboolean e_file_lock_exists (void); + +gchar * e_util_guess_mime_type (const gchar *filename, + gboolean localfile); + +GSList * e_util_get_category_filter_options + (void); +GList * e_util_get_searchable_categories (void); + +/* Useful GBinding transform functions */ +gboolean e_binding_transform_color_to_string + (GBinding *binding, + const GValue *source_value, + GValue *target_value, + gpointer not_used); +gboolean e_binding_transform_string_to_color + (GBinding *binding, + const GValue *source_value, + GValue *target_value, + gpointer not_used); +gboolean e_binding_transform_source_to_uid + (GBinding *binding, + const GValue *source_value, + GValue *target_value, + ESourceRegistry *registry); +gboolean e_binding_transform_uid_to_source + (GBinding *binding, + const GValue *source_value, + GValue *target_value, + ESourceRegistry *registry); + +G_END_DECLS + +#endif /* E_MISC_UTILS_H */ diff --git a/e-util/e-mktemp.c b/e-util/e-mktemp.c index 9b68ccc473..f5042fa6a2 100644 --- a/e-util/e-mktemp.c +++ b/e-util/e-mktemp.c @@ -35,7 +35,8 @@ #include <stdio.h> #include <time.h> -#include "e-util.h" +#include <libedataserver/libedataserver.h> + #include "e-mktemp.h" #define d(x) diff --git a/e-util/e-mktemp.h b/e-util/e-mktemp.h index 08f75ea67e..6c05541611 100644 --- a/e-util/e-mktemp.h +++ b/e-util/e-mktemp.h @@ -20,6 +20,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef __E_MKTEMP_H__ #define __E_MKTEMP_H__ diff --git a/e-util/e-name-selector-dialog.c b/e-util/e-name-selector-dialog.c new file mode 100644 index 0000000000..ece556b0a9 --- /dev/null +++ b/e-util/e-name-selector-dialog.c @@ -0,0 +1,1863 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-name-selector-dialog.c - Dialog that lets user pick EDestinations. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Hans Petter Jansson <hpj@novell.com> + */ + +#ifdef GTK_DISABLE_DEPRECATED +#undef GTK_DISABLE_DEPRECATED +#endif + +#include <config.h> +#include <string.h> +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n-lib.h> + +#include <libebook/libebook.h> +#include <libebackend/libebackend.h> + +#include "e-source-combo-box.h" +#include "e-destination-store.h" +#include "e-contact-store.h" +#include "e-client-utils.h" +#include "e-name-selector-dialog.h" +#include "e-name-selector-entry.h" + +#define E_NAME_SELECTOR_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_NAME_SELECTOR_DIALOG, ENameSelectorDialogPrivate)) + +typedef struct { + gchar *name; + + GtkGrid *section_grid; + GtkLabel *label; + GtkButton *transfer_button; + GtkButton *remove_button; + GtkTreeView *destination_view; +} +Section; + +typedef struct { + GtkTreeView *view; + GtkButton *button; + ENameSelectorDialog *dlg_ptr; +} SelData; + +struct _ENameSelectorDialogPrivate { + ESourceRegistry *registry; + ENameSelectorModel *name_selector_model; + GtkTreeModelSort *contact_sort; + GCancellable *cancellable; + + GtkTreeView *contact_view; + GtkLabel *status_label; + GtkGrid *destination_vgrid; + GtkEntry *search_entry; + GtkSizeGroup *button_size_group; + GtkWidget *category_combobox; + GtkWidget *contact_window; + + GArray *sections; + + guint destination_index; + GSList *user_query_fields; + GtkSizeGroup *dest_label_size_group; +}; + +enum { + PROP_0, + PROP_REGISTRY +}; + +static void search_changed (ENameSelectorDialog *name_selector_dialog); +static void source_changed (ENameSelectorDialog *name_selector_dialog, ESourceComboBox *source_combo_box); +static void transfer_button_clicked (ENameSelectorDialog *name_selector_dialog, GtkButton *transfer_button); +static void contact_selection_changed (ENameSelectorDialog *name_selector_dialog); +static void setup_name_selector_model (ENameSelectorDialog *name_selector_dialog); +static void shutdown_name_selector_model (ENameSelectorDialog *name_selector_dialog); +static void contact_activated (ENameSelectorDialog *name_selector_dialog, GtkTreePath *path); +static void destination_activated (ENameSelectorDialog *name_selector_dialog, GtkTreePath *path, + GtkTreeViewColumn *column, GtkTreeView *tree_view); +static gboolean destination_key_press (ENameSelectorDialog *name_selector_dialog, GdkEventKey *event, GtkTreeView *tree_view); +static void remove_button_clicked (GtkButton *button, SelData *data); +static void remove_books (ENameSelectorDialog *name_selector_dialog); +static void contact_column_formatter (GtkTreeViewColumn *column, GtkCellRenderer *cell, + GtkTreeModel *model, GtkTreeIter *iter, + ENameSelectorDialog *name_selector_dialog); +static void destination_column_formatter (GtkTreeViewColumn *column, GtkCellRenderer *cell, + GtkTreeModel *model, GtkTreeIter *iter, + ENameSelectorDialog *name_selector_dialog); + +/* ------------------ * + * Class/object setup * + * ------------------ */ + +G_DEFINE_TYPE_WITH_CODE ( + ENameSelectorDialog, + e_name_selector_dialog, + GTK_TYPE_DIALOG, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +static void +name_selector_dialog_populate_categories (ENameSelectorDialog *name_selector_dialog) +{ + GtkWidget *combo_box; + GList *category_list, *iter; + + /* "Any Category" is preloaded. */ + combo_box = name_selector_dialog->priv->category_combobox; + if (gtk_combo_box_get_active (GTK_COMBO_BOX (combo_box)) == -1) + gtk_combo_box_set_active (GTK_COMBO_BOX (combo_box), 0); + + /* Categories are already sorted. */ + category_list = e_categories_get_list (); + for (iter = category_list; iter != NULL; iter = iter->next) { + /* Only add user-visible categories. */ + if (!e_categories_is_searchable (iter->data)) + continue; + + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (combo_box), iter->data); + } + + g_list_free (category_list); + + g_signal_connect_swapped ( + combo_box, "changed", + G_CALLBACK (search_changed), name_selector_dialog); +} + +static void +name_selector_dialog_set_registry (ENameSelectorDialog *name_selector_dialog, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (name_selector_dialog->priv->registry == NULL); + + name_selector_dialog->priv->registry = g_object_ref (registry); +} + +static void +name_selector_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + name_selector_dialog_set_registry ( + E_NAME_SELECTOR_DIALOG (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +name_selector_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + g_value_set_object ( + value, + e_name_selector_dialog_get_registry ( + E_NAME_SELECTOR_DIALOG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +name_selector_dialog_dispose (GObject *object) +{ + ENameSelectorDialogPrivate *priv; + + priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (object); + + remove_books (E_NAME_SELECTOR_DIALOG (object)); + shutdown_name_selector_model (E_NAME_SELECTOR_DIALOG (object)); + + if (priv->registry != NULL) { + g_object_unref (priv->registry); + priv->registry = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_name_selector_dialog_parent_class)->dispose (object); +} + +static void +name_selector_dialog_finalize (GObject *object) +{ + ENameSelectorDialogPrivate *priv; + + priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (object); + + g_slist_foreach (priv->user_query_fields, (GFunc) g_free, NULL); + g_slist_free (priv->user_query_fields); + + g_array_free (priv->sections, TRUE); + g_object_unref (priv->button_size_group); + g_object_unref (priv->dest_label_size_group); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_name_selector_dialog_parent_class)->finalize (object); +} + +static void +name_selector_dialog_constructed (GObject *object) +{ + ENameSelectorDialogPrivate *priv; + GtkTreeSelection *contact_selection; + GtkTreeViewColumn *column; + GtkCellRenderer *cell_renderer; + GtkTreeSelection *selection; + ESource *source; + gchar *tmp_str; + GtkWidget *name_selector_grid; + GtkWidget *show_contacts_label; + GtkWidget *hgrid; + GtkWidget *label; + GtkWidget *show_contacts_grid; + GtkWidget *AddressBookLabel; + GtkWidget *label_category; + GtkWidget *search; + AtkObject *atko; + GtkWidget *label_search; + GtkWidget *source_menu_hgrid; + GtkWidget *combobox_category; + GtkWidget *label_contacts; + GtkWidget *scrolledwindow0; + GtkWidget *scrolledwindow1; + AtkRelationSet *tmp_relation_set; + AtkRelationType tmp_relationship; + AtkRelation *tmp_relation; + AtkObject *scrolledwindow1_relation_targets[1]; + GtkWidget *source_tree_view; + GtkWidget *destination_vgrid; + GtkWidget *status_message; + GtkWidget *source_combo; + const gchar *extension_name; + + priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_name_selector_dialog_parent_class)->constructed (object); + + name_selector_grid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_VERTICAL, + "column-homogeneous", FALSE, + "row-spacing", 6, + NULL); + gtk_widget_show (name_selector_grid); + gtk_container_set_border_width (GTK_CONTAINER (name_selector_grid), 0); + + tmp_str = g_strconcat ("<b>", _("Show Contacts"), "</b>", NULL); + show_contacts_label = gtk_label_new (tmp_str); + gtk_widget_show (show_contacts_label); + gtk_container_add (GTK_CONTAINER (name_selector_grid), show_contacts_label); + gtk_label_set_use_markup (GTK_LABEL (show_contacts_label), TRUE); + gtk_misc_set_alignment (GTK_MISC (show_contacts_label), 0, 0.5); + g_free (tmp_str); + + hgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "row-homogeneous", FALSE, + "column-spacing", 12, + NULL); + gtk_widget_show (hgrid); + gtk_container_add (GTK_CONTAINER (name_selector_grid), hgrid); + + label = gtk_label_new (""); + gtk_widget_show (label); + gtk_container_add (GTK_CONTAINER (hgrid), label); + + show_contacts_grid = gtk_grid_new (); + gtk_widget_show (show_contacts_grid); + gtk_container_add (GTK_CONTAINER (hgrid), show_contacts_grid); + g_object_set (G_OBJECT (show_contacts_grid), + "column-spacing", 12, + "row-spacing", 6, + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + NULL); + + AddressBookLabel = gtk_label_new_with_mnemonic (_("Address B_ook:")); + gtk_widget_show (AddressBookLabel); + gtk_grid_attach (GTK_GRID (show_contacts_grid), AddressBookLabel, 0, 0, 1, 1); + gtk_widget_set_halign (AddressBookLabel, GTK_ALIGN_FILL); + gtk_label_set_justify (GTK_LABEL (AddressBookLabel), GTK_JUSTIFY_CENTER); + gtk_misc_set_alignment (GTK_MISC (AddressBookLabel), 0, 0.5); + + label_category = gtk_label_new_with_mnemonic (_("Cat_egory:")); + gtk_widget_show (label_category); + gtk_grid_attach (GTK_GRID (show_contacts_grid), label_category, 0, 1, 1, 1); + gtk_widget_set_halign (label_category, GTK_ALIGN_FILL); + gtk_label_set_justify (GTK_LABEL (label_category), GTK_JUSTIFY_CENTER); + gtk_misc_set_alignment (GTK_MISC (label_category), 0, 0.5); + + hgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "row-homogeneous", FALSE, + "column-spacing", 12, + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (hgrid); + gtk_grid_attach (GTK_GRID (show_contacts_grid), hgrid, 1, 2, 1, 1); + + search = gtk_entry_new (); + gtk_widget_show (search); + gtk_widget_set_hexpand (search, TRUE); + gtk_widget_set_halign (search, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (hgrid), search); + + label_search = gtk_label_new_with_mnemonic (_("_Search:")); + gtk_widget_show (label_search); + gtk_grid_attach (GTK_GRID (show_contacts_grid), label_search, 0, 2, 1, 1); + gtk_widget_set_halign (label_search, GTK_ALIGN_FILL); + gtk_misc_set_alignment (GTK_MISC (label_search), 0, 0.5); + + source_menu_hgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "row-homogeneous", FALSE, + "column-spacing", 0, + "halign", GTK_ALIGN_FILL, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (source_menu_hgrid); + gtk_grid_attach (GTK_GRID (show_contacts_grid), source_menu_hgrid, 1, 0, 1, 1); + + combobox_category = gtk_combo_box_text_new (); + gtk_widget_show (combobox_category); + g_object_set (G_OBJECT (combobox_category), + "halign", GTK_ALIGN_FILL, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_grid_attach (GTK_GRID (show_contacts_grid), combobox_category, 1, 1, 1, 1); + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (combobox_category), _("Any Category")); + + tmp_str = g_strconcat ("<b>", _("Co_ntacts"), "</b>", NULL); + label_contacts = gtk_label_new_with_mnemonic (tmp_str); + gtk_widget_show (label_contacts); + gtk_container_add (GTK_CONTAINER (name_selector_grid), label_contacts); + gtk_label_set_use_markup (GTK_LABEL (label_contacts), TRUE); + gtk_misc_set_alignment (GTK_MISC (label_contacts), 0, 0.5); + g_free (tmp_str); + + scrolledwindow0 = gtk_scrolled_window_new (NULL, NULL); + priv->contact_window = scrolledwindow0; + gtk_widget_show (scrolledwindow0); + gtk_widget_set_vexpand (scrolledwindow0, TRUE); + gtk_widget_set_valign (scrolledwindow0, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (name_selector_grid), scrolledwindow0); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (scrolledwindow0), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + + hgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "row-homogeneous", FALSE, + "column-spacing", 12, + NULL); + gtk_widget_show (hgrid); + gtk_scrolled_window_add_with_viewport ( + GTK_SCROLLED_WINDOW (scrolledwindow0), hgrid); + + label = gtk_label_new (""); + gtk_widget_show (label); + gtk_container_add (GTK_CONTAINER (hgrid), label); + + scrolledwindow1 = gtk_scrolled_window_new (NULL, NULL); + gtk_widget_show (scrolledwindow1); + gtk_container_add (GTK_CONTAINER (hgrid), scrolledwindow1); + gtk_widget_set_hexpand (scrolledwindow1, TRUE); + gtk_widget_set_halign (scrolledwindow1, GTK_ALIGN_FILL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (scrolledwindow1), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (scrolledwindow1), GTK_SHADOW_IN); + + source_tree_view = gtk_tree_view_new (); + gtk_widget_show (source_tree_view); + gtk_container_add (GTK_CONTAINER (scrolledwindow1), source_tree_view); + gtk_tree_view_set_headers_visible ( + GTK_TREE_VIEW (source_tree_view), FALSE); + gtk_tree_view_set_enable_search ( + GTK_TREE_VIEW (source_tree_view), FALSE); + + destination_vgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_VERTICAL, + "column-homogeneous", TRUE, + "row-spacing", 6, + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (destination_vgrid); + gtk_container_add (GTK_CONTAINER (hgrid), destination_vgrid); + + status_message = gtk_label_new (""); + gtk_widget_show (status_message); + gtk_container_add (GTK_CONTAINER (name_selector_grid), status_message); + gtk_label_set_use_markup (GTK_LABEL (status_message), TRUE); + gtk_misc_set_alignment (GTK_MISC (status_message), 0, 0.5); + gtk_misc_set_padding (GTK_MISC (status_message), 0, 3); + + gtk_label_set_mnemonic_widget (GTK_LABEL (AddressBookLabel), source_menu_hgrid); + gtk_label_set_mnemonic_widget (GTK_LABEL (label_category), combobox_category); + gtk_label_set_mnemonic_widget (GTK_LABEL (label_search), search); + gtk_label_set_mnemonic_widget (GTK_LABEL (label_contacts), source_tree_view); + + atko = gtk_widget_get_accessible (search); + atk_object_set_name (atko, _("Search")); + + atko = gtk_widget_get_accessible (source_menu_hgrid); + atk_object_set_name (atko, _("Address Book")); + + atko = gtk_widget_get_accessible (scrolledwindow1); + atk_object_set_name (atko, _("Contacts")); + tmp_relation_set = atk_object_ref_relation_set (atko); + scrolledwindow1_relation_targets[0] = gtk_widget_get_accessible (label_contacts); + tmp_relationship = atk_relation_type_for_name ("labelled-by"); + tmp_relation = atk_relation_new (scrolledwindow1_relation_targets, 1, tmp_relationship); + atk_relation_set_add (tmp_relation_set, tmp_relation); + g_object_unref (G_OBJECT (tmp_relation)); + g_object_unref (G_OBJECT (tmp_relation_set)); + + gtk_box_pack_start ( + GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (object))), + name_selector_grid, TRUE, TRUE, 0); + + /* Store pointers to relevant widgets */ + + priv->contact_view = GTK_TREE_VIEW (source_tree_view); + priv->status_label = GTK_LABEL (status_message); + priv->destination_vgrid = GTK_GRID (destination_vgrid); + priv->search_entry = GTK_ENTRY (search); + priv->category_combobox = combobox_category; + + /* Create size group for transfer buttons */ + + priv->button_size_group = + gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + /* Create size group for destination labels */ + + priv->dest_label_size_group = + gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + /* Set up contacts view */ + + column = gtk_tree_view_column_new (); + cell_renderer = GTK_CELL_RENDERER (gtk_cell_renderer_text_new ()); + gtk_tree_view_column_pack_start (column, cell_renderer, TRUE); + gtk_tree_view_column_set_cell_data_func ( + column, cell_renderer, (GtkTreeCellDataFunc) + contact_column_formatter, object, NULL); + + selection = gtk_tree_view_get_selection (priv->contact_view); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE); + gtk_tree_view_append_column (priv->contact_view, column); + g_signal_connect_swapped ( + priv->contact_view, "row-activated", + G_CALLBACK (contact_activated), object); + + /* Listen for changes to the contact selection */ + + contact_selection = gtk_tree_view_get_selection (priv->contact_view); + g_signal_connect_swapped ( + contact_selection, "changed", + G_CALLBACK (contact_selection_changed), object); + + /* Set up our data structures */ + + priv->name_selector_model = e_name_selector_model_new (); + priv->sections = g_array_new (FALSE, FALSE, sizeof (Section)); + + setup_name_selector_model (E_NAME_SELECTOR_DIALOG (object)); + + /* Create source menu */ + + extension_name = E_SOURCE_EXTENSION_ADDRESS_BOOK; + source_combo = e_source_combo_box_new (priv->registry, extension_name); + g_signal_connect_swapped ( + source_combo, "changed", + G_CALLBACK (source_changed), object); + + source_changed (E_NAME_SELECTOR_DIALOG (object), E_SOURCE_COMBO_BOX (source_combo)); + + gtk_label_set_mnemonic_widget (GTK_LABEL (AddressBookLabel), source_combo); + gtk_widget_show (source_combo); + gtk_widget_set_hexpand (source_combo, TRUE); + gtk_widget_set_halign (source_combo, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (source_menu_hgrid), source_combo); + + name_selector_dialog_populate_categories ( + E_NAME_SELECTOR_DIALOG (object)); + + /* Set up search-as-you-type signal */ + + g_signal_connect_swapped ( + search, "changed", + G_CALLBACK (search_changed), object); + + /* Display initial source */ + + source = e_source_registry_ref_default_address_book (priv->registry); + e_source_combo_box_set_active ( + E_SOURCE_COMBO_BOX (source_combo), source); + g_object_unref (source); + + /* Set up dialog defaults */ + + gtk_dialog_add_buttons ( + GTK_DIALOG (object), + GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, + NULL); + + /* Try to figure out a sane default size for the dialog. We used to hard + * code this to 512 so keep using 512 if the screen is big enough, + * otherwise use -1 (use as little as possible, use the + * GtkScrolledWindow's scrollbars). + * + * This should allow scrolling on tiny netbook resolutions and let + * others see as much of the dialog as possible. + * + * 600 pixels seems to be a good lower bound resolution to allow room + * above or below for other UI (window manager's?) + */ + gtk_window_set_default_size ( + GTK_WINDOW (object), 700, + gdk_screen_height () >= 600 ? 512 : -1); + + gtk_dialog_set_default_response ( + GTK_DIALOG (object), GTK_RESPONSE_CLOSE); + gtk_window_set_modal (GTK_WINDOW (object), TRUE); + gtk_window_set_resizable (GTK_WINDOW (object), TRUE); + gtk_container_set_border_width (GTK_CONTAINER (object), 4); + gtk_window_set_title ( + GTK_WINDOW (object), + _("Select Contacts from Address Book")); + gtk_widget_grab_focus (search); + + e_extensible_load_extensions (E_EXTENSIBLE (object)); +} + +static void +e_name_selector_dialog_class_init (ENameSelectorDialogClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ENameSelectorDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = name_selector_dialog_set_property; + object_class->get_property = name_selector_dialog_get_property; + object_class->dispose = name_selector_dialog_dispose; + object_class->finalize = name_selector_dialog_finalize; + object_class->constructed = name_selector_dialog_constructed; + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + "Data source registry", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_name_selector_dialog_init (ENameSelectorDialog *name_selector_dialog) +{ + name_selector_dialog->priv = + E_NAME_SELECTOR_DIALOG_GET_PRIVATE (name_selector_dialog); +} + +/** + * e_name_selector_dialog_new: + * @registry: an #ESourceRegistry + * + * Creates a new #ENameSelectorDialog. + * + * Returns: A new #ENameSelectorDialog. + **/ +ENameSelectorDialog * +e_name_selector_dialog_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_NAME_SELECTOR_DIALOG, + "registry", registry, NULL); +} + +/** + * e_name_selector_dialog_get_registry: + * @name_selector_dialog: an #ENameSelectorDialog + * + * Returns the #ESourceRegistry that was passed to + * e_name_selector_dialog_new(). + * + * Returns: the #ESourceRegistry + * + * Since: 3.6 + **/ +ESourceRegistry * +e_name_selector_dialog_get_registry (ENameSelectorDialog *name_selector_dialog) +{ + g_return_val_if_fail ( + E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog), NULL); + + return name_selector_dialog->priv->registry; +} + +/* --------- * + * Utilities * + * --------- */ + +static gchar * +escape_sexp_string (const gchar *string) +{ + GString *gstring; + gchar *encoded_string; + + gstring = g_string_new (""); + e_sexp_encode_string (gstring, string); + + encoded_string = gstring->str; + g_string_free (gstring, FALSE); + + return encoded_string; +} + +static void +sort_iter_to_contact_store_iter (ENameSelectorDialog *name_selector_dialog, + GtkTreeIter *iter, + gint *email_n) +{ + ETreeModelGenerator *contact_filter; + GtkTreeIter child_iter; + gint email_n_local; + + contact_filter = e_name_selector_model_peek_contact_filter ( + name_selector_dialog->priv->name_selector_model); + + gtk_tree_model_sort_convert_iter_to_child_iter ( + name_selector_dialog->priv->contact_sort, &child_iter, iter); + e_tree_model_generator_convert_iter_to_child_iter ( + contact_filter, iter, &email_n_local, &child_iter); + + if (email_n) + *email_n = email_n_local; +} + +static void +add_destination (ENameSelectorModel *name_selector_model, + EDestinationStore *destination_store, + EContact *contact, + gint email_n, + EBookClient *client) +{ + EDestination *destination; + GList *email_list, *nth; + + /* get the correct index of an email in the contact */ + email_list = e_name_selector_model_get_contact_emails_without_used (name_selector_model, contact, FALSE); + while (nth = g_list_nth (email_list, email_n), nth && nth->data == NULL) { + email_n++; + } + e_name_selector_model_free_emails_list (email_list); + + /* Transfer (actually, copy into a destination and let the model filter out the + * source automatically) */ + + destination = e_destination_new (); + e_destination_set_contact (destination, contact, email_n); + if (client) + e_destination_set_client (destination, client); + e_destination_store_append_destination (destination_store, destination); + g_object_unref (destination); +} + +static void +remove_books (ENameSelectorDialog *name_selector_dialog) +{ + EContactStore *contact_store; + GSList *clients, *l; + + if (!name_selector_dialog->priv->name_selector_model) + return; + + contact_store = e_name_selector_model_peek_contact_store ( + name_selector_dialog->priv->name_selector_model); + + /* Remove books (should be just one) being viewed */ + clients = e_contact_store_get_clients (contact_store); + for (l = clients; l; l = g_slist_next (l)) { + EBookClient *client = l->data; + e_contact_store_remove_client (contact_store, client); + } + g_slist_free (clients); + + /* See if we have a book pending; stop loading it if so */ + if (name_selector_dialog->priv->cancellable != NULL) { + g_cancellable_cancel (name_selector_dialog->priv->cancellable); + g_object_unref (name_selector_dialog->priv->cancellable); + name_selector_dialog->priv->cancellable = NULL; + } +} + +/* ------------------ * + * Section management * + * ------------------ */ + +static gint +find_section_by_transfer_button (ENameSelectorDialog *name_selector_dialog, + GtkButton *transfer_button) +{ + gint i; + + for (i = 0; i < name_selector_dialog->priv->sections->len; i++) { + Section *section = &g_array_index ( + name_selector_dialog->priv->sections, Section, i); + + if (section->transfer_button == transfer_button) + return i; + } + + return -1; +} + +static gint +find_section_by_tree_view (ENameSelectorDialog *name_selector_dialog, + GtkTreeView *tree_view) +{ + gint i; + + for (i = 0; i < name_selector_dialog->priv->sections->len; i++) { + Section *section = &g_array_index ( + name_selector_dialog->priv->sections, Section, i); + + if (section->destination_view == tree_view) + return i; + } + + return -1; +} + +static gint +find_section_by_name (ENameSelectorDialog *name_selector_dialog, + const gchar *name) +{ + gint i; + + for (i = 0; i < name_selector_dialog->priv->sections->len; i++) { + Section *section = &g_array_index ( + name_selector_dialog->priv->sections, Section, i); + + if (!strcmp (name, section->name)) + return i; + } + + return -1; +} + +static void +selection_changed (GtkTreeSelection *selection, + SelData *data) +{ + GtkTreeSelection *contact_selection; + gboolean have_selection = FALSE; + + contact_selection = gtk_tree_view_get_selection (data->view); + if (gtk_tree_selection_count_selected_rows (contact_selection) > 0) + have_selection = TRUE; + gtk_widget_set_sensitive (GTK_WIDGET (data->button), have_selection); +} + +static GtkTreeView * +make_tree_view_for_section (ENameSelectorDialog *name_selector_dialog, + EDestinationStore *destination_store) +{ + GtkTreeView *tree_view; + GtkTreeViewColumn *column; + GtkCellRenderer *cell_renderer; + + tree_view = GTK_TREE_VIEW (gtk_tree_view_new ()); + + column = gtk_tree_view_column_new (); + cell_renderer = GTK_CELL_RENDERER (gtk_cell_renderer_text_new ()); + gtk_tree_view_column_pack_start (column, cell_renderer, TRUE); + gtk_tree_view_column_set_cell_data_func ( + column, cell_renderer, + (GtkTreeCellDataFunc) destination_column_formatter, + name_selector_dialog, NULL); + gtk_tree_view_append_column (tree_view, column); + gtk_tree_view_set_headers_visible (tree_view, FALSE); + gtk_tree_view_set_model (tree_view, GTK_TREE_MODEL (destination_store)); + + return tree_view; +} + +static void +setup_section_button (ENameSelectorDialog *name_selector_dialog, + GtkButton *button, + double halign, + const gchar *label_text, + const gchar *icon_name, + gboolean icon_before_label) +{ + GtkWidget *alignment; + GtkWidget *hgrid; + GtkWidget *label; + GtkWidget *image; + + gtk_size_group_add_widget ( + name_selector_dialog->priv->button_size_group, + GTK_WIDGET (button)); + + alignment = gtk_alignment_new (halign, 0.5, 0.0, 0.0); + gtk_container_add (GTK_CONTAINER (button), GTK_WIDGET (alignment)); + + hgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "row-homogeneous", FALSE, + "column-spacing", 2, + NULL); + gtk_widget_show (hgrid); + gtk_container_add (GTK_CONTAINER (alignment), hgrid); + + label = gtk_label_new_with_mnemonic (label_text); + gtk_widget_show (label); + + image = gtk_image_new_from_stock (icon_name, GTK_ICON_SIZE_BUTTON); + gtk_widget_show (image); + + if (icon_before_label) { + gtk_container_add (GTK_CONTAINER (hgrid), image); + gtk_container_add (GTK_CONTAINER (hgrid), label); + } else { + gtk_container_add (GTK_CONTAINER (hgrid), label); + gtk_container_add (GTK_CONTAINER (hgrid), image); + } +} + +static gint +add_section (ENameSelectorDialog *name_selector_dialog, + const gchar *name, + const gchar *pretty_name, + EDestinationStore *destination_store) +{ + ENameSelectorDialogPrivate *priv; + Section section; + GtkWidget *vgrid; + GtkWidget *alignment; + GtkWidget *scrollwin; + SelData *data; + GtkTreeSelection *selection; + gchar *text; + GtkWidget *hgrid; + + g_assert (name != NULL); + g_assert (pretty_name != NULL); + g_assert (E_IS_DESTINATION_STORE (destination_store)); + + priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (name_selector_dialog); + + memset (§ion, 0, sizeof (Section)); + + section.name = g_strdup (name); + section.section_grid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "row-homogeneous", FALSE, + "column-spacing", 12, + "vexpand", TRUE, + "valign", GTK_ALIGN_FILL, + NULL); + section.label = GTK_LABEL (gtk_label_new_with_mnemonic (pretty_name)); + section.transfer_button = GTK_BUTTON (gtk_button_new ()); + section.remove_button = GTK_BUTTON (gtk_button_new ()); + section.destination_view = make_tree_view_for_section (name_selector_dialog, destination_store); + + gtk_label_set_mnemonic_widget (GTK_LABEL (section.label), GTK_WIDGET (section.destination_view)); + + if (pango_parse_markup (pretty_name, -1, '_', NULL, + &text, NULL, NULL)) { + atk_object_set_name (gtk_widget_get_accessible ( + GTK_WIDGET (section.destination_view)), text); + g_free (text); + } + + /* Set up transfer button */ + g_signal_connect_swapped ( + section.transfer_button, "clicked", + G_CALLBACK (transfer_button_clicked), name_selector_dialog); + + /*data for the remove callback*/ + data = g_malloc0 (sizeof (SelData)); + data->view = section.destination_view; + data->dlg_ptr = name_selector_dialog; + + /*Associate to an object destroy so that it gets freed*/ + g_object_set_data_full ((GObject *) section.destination_view, "sel-remove-data", data, g_free); + + g_signal_connect ( + section.remove_button, "clicked", + G_CALLBACK (remove_button_clicked), data); + + /* Alignment and vgrid for the add/remove buttons */ + + alignment = gtk_alignment_new (0.5, 0.0, 0.0, 0.0); + gtk_container_add (GTK_CONTAINER (section.section_grid), alignment); + + vgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_VERTICAL, + "column-homogeneous", TRUE, + "row-spacing", 6, + NULL); + + gtk_container_add (GTK_CONTAINER (alignment), vgrid); + + /* "Add" button */ + gtk_container_add (GTK_CONTAINER (vgrid), GTK_WIDGET (section.transfer_button)); + setup_section_button (name_selector_dialog, section.transfer_button, 0.7, _("_Add"), "gtk-go-forward", FALSE); + + /* "Remove" button */ + gtk_container_add (GTK_CONTAINER (vgrid), GTK_WIDGET (section.remove_button)); + setup_section_button (name_selector_dialog, section.remove_button, 0.5, _("_Remove"), "gtk-go-back", TRUE); + gtk_widget_set_sensitive (GTK_WIDGET (section.remove_button), FALSE); + + /* hgrid for label and scrolled window. This is a separate hgrid, instead + * of just using the section.section_grid directly, as it has a different + * spacing. + */ + + hgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "row-homogeneous", FALSE, + "column-spacing", 6, + "vexpand", TRUE, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_container_add (GTK_CONTAINER (section.section_grid), hgrid); + + /* Title label */ + + gtk_size_group_add_widget (priv->dest_label_size_group, GTK_WIDGET (section.label)); + + gtk_misc_set_alignment (GTK_MISC (section.label), 0.0, 0.0); + gtk_container_add (GTK_CONTAINER (hgrid), GTK_WIDGET (section.label)); + + /* Treeview in a scrolled window */ + scrollwin = gtk_scrolled_window_new (NULL, NULL); + gtk_container_add (GTK_CONTAINER (hgrid), scrollwin); + gtk_widget_set_hexpand (scrollwin, TRUE); + gtk_widget_set_halign (scrollwin, GTK_ALIGN_FILL); + gtk_widget_set_vexpand (scrollwin, TRUE); + gtk_widget_set_valign (scrollwin, GTK_ALIGN_FILL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrollwin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrollwin), GTK_SHADOW_IN); + gtk_container_add (GTK_CONTAINER (scrollwin), GTK_WIDGET (section.destination_view)); + + /*data for 'changed' callback*/ + data = g_malloc0 (sizeof (SelData)); + data->view = section.destination_view; + data->button = section.remove_button; + g_object_set_data_full ((GObject *) section.destination_view, "sel-change-data", data, g_free); + selection = gtk_tree_view_get_selection (section.destination_view); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE); + + g_signal_connect ( + selection, "changed", + G_CALLBACK (selection_changed), data); + + g_signal_connect_swapped ( + section.destination_view, "row-activated", + G_CALLBACK (destination_activated), name_selector_dialog); + g_signal_connect_swapped ( + section.destination_view, "key-press-event", + G_CALLBACK (destination_key_press), name_selector_dialog); + + /* Done! */ + + gtk_widget_show_all (GTK_WIDGET (section.section_grid)); + + /* Pack this section's box into the dialog */ + gtk_container_add (GTK_CONTAINER (name_selector_dialog->priv->destination_vgrid), GTK_WIDGET (section.section_grid)); + g_object_set (G_OBJECT (section.section_grid), + "vexpand", TRUE, + "valign", GTK_ALIGN_FILL, + NULL); + + g_array_append_val (name_selector_dialog->priv->sections, section); + + /* Make sure UI is consistent */ + contact_selection_changed (name_selector_dialog); + + return name_selector_dialog->priv->sections->len - 1; +} + +static void +free_section (ENameSelectorDialog *name_selector_dialog, + gint n) +{ + Section *section; + + g_assert (n >= 0); + g_assert (n < name_selector_dialog->priv->sections->len); + + section = &g_array_index ( + name_selector_dialog->priv->sections, Section, n); + + g_free (section->name); + gtk_widget_destroy (GTK_WIDGET (section->section_grid)); +} + +static void +model_section_added (ENameSelectorDialog *name_selector_dialog, + const gchar *name) +{ + gchar *pretty_name; + EDestinationStore *destination_store; + + e_name_selector_model_peek_section ( + name_selector_dialog->priv->name_selector_model, + name, &pretty_name, &destination_store); + add_section (name_selector_dialog, name, pretty_name, destination_store); + g_free (pretty_name); +} + +static void +model_section_removed (ENameSelectorDialog *name_selector_dialog, + const gchar *name) +{ + gint section_index; + + section_index = find_section_by_name (name_selector_dialog, name); + g_assert (section_index >= 0); + + free_section (name_selector_dialog, section_index); + g_array_remove_index ( + name_selector_dialog->priv->sections, section_index); +} + +/* -------------------- * + * Addressbook selector * + * -------------------- */ + +static void +view_progress (EBookClientView *view, + guint percent, + const gchar *message, + ENameSelectorDialog *dialog) +{ + if (message == NULL) + gtk_label_set_text (dialog->priv->status_label, ""); + else + gtk_label_set_text (dialog->priv->status_label, message); +} + +static void +view_complete (EBookClientView *view, + const GError *error, + ENameSelectorDialog *dialog) +{ + view_progress (view, -1, NULL, dialog); +} + +static void +start_client_view_cb (EContactStore *store, + EBookClientView *client_view, + ENameSelectorDialog *name_selector_dialog) +{ + g_signal_connect ( + client_view, "progress", + G_CALLBACK (view_progress), name_selector_dialog); + + g_signal_connect ( + client_view, "complete", + G_CALLBACK (view_complete), name_selector_dialog); +} + +static void +stop_client_view_cb (EContactStore *store, + EBookClientView *client_view, + ENameSelectorDialog *name_selector_dialog) +{ + g_signal_handlers_disconnect_by_func (client_view, view_progress, name_selector_dialog); + g_signal_handlers_disconnect_by_func (client_view, view_complete, name_selector_dialog); +} + +static void +book_loaded_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + ENameSelectorDialog *name_selector_dialog = user_data; + EClient *client = NULL; + EBookClient *book_client; + EContactStore *store; + ENameSelectorModel *model; + GError *error = NULL; + + e_client_utils_open_new_finish (E_SOURCE (source_object), result, &client, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warn_if_fail (client == NULL); + g_error_free (error); + goto exit; + } + + if (error != NULL) { + gchar *message; + + message = g_strdup_printf ( + _("Error loading address book: %s"), error->message); + gtk_label_set_text ( + name_selector_dialog->priv->status_label, message); + g_free (message); + + g_warn_if_fail (client == NULL); + g_error_free (error); + goto exit; + } + + book_client = E_BOOK_CLIENT (client); + if (!book_client) { + g_warn_if_fail (book_client != NULL); + goto exit; + } + + model = name_selector_dialog->priv->name_selector_model; + store = e_name_selector_model_peek_contact_store (model); + e_contact_store_add_client (store, book_client); + g_object_unref (book_client); + + exit: + g_object_unref (name_selector_dialog); +} + +static void +source_changed (ENameSelectorDialog *name_selector_dialog, + ESourceComboBox *source_combo_box) +{ + GCancellable *cancellable; + ESource *source; + gpointer parent; + + source = e_source_combo_box_ref_active (source_combo_box); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (name_selector_dialog)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + /* Remove any previous books being shown or loaded */ + remove_books (name_selector_dialog); + + if (source == NULL) + return; + + cancellable = g_cancellable_new (); + name_selector_dialog->priv->cancellable = cancellable; + + /* Start loading selected book */ + e_client_utils_open_new ( + source, E_CLIENT_SOURCE_TYPE_CONTACTS, TRUE, cancellable, + book_loaded_cb, g_object_ref (name_selector_dialog)); + + g_object_unref (source); +} + +/* --------------- * + * Other UI events * + * --------------- */ + +static void +search_changed (ENameSelectorDialog *name_selector_dialog) +{ + ENameSelectorDialogPrivate *priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (name_selector_dialog); + EContactStore *contact_store; + EBookQuery *book_query; + GtkWidget *combo_box; + const gchar *text; + gchar *text_escaped; + gchar *query_string; + gchar *category; + gchar *category_escaped; + gchar *user_fields_str; + + combo_box = priv->category_combobox; + if (gtk_combo_box_get_active (GTK_COMBO_BOX (combo_box)) == -1) + gtk_combo_box_set_active (GTK_COMBO_BOX (combo_box), 0); + + category = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (combo_box)); + category_escaped = escape_sexp_string (category); + + text = gtk_entry_get_text (name_selector_dialog->priv->search_entry); + text_escaped = escape_sexp_string (text); + + user_fields_str = ens_util_populate_user_query_fields (priv->user_query_fields, text, text_escaped); + + if (g_strcmp0 (category, _("Any Category")) == 0) + query_string = g_strdup_printf ( + "(or (beginswith \"file_as\" %s) " + " (beginswith \"full_name\" %s) " + " (beginswith \"email\" %s) " + " (beginswith \"nickname\" %s)%s))", + text_escaped, text_escaped, + text_escaped, text_escaped, + user_fields_str ? user_fields_str : ""); + else + query_string = g_strdup_printf ( + "(and (is \"category_list\" %s) " + "(or (beginswith \"file_as\" %s) " + " (beginswith \"full_name\" %s) " + " (beginswith \"email\" %s) " + " (beginswith \"nickname\" %s)%s))", + category_escaped, text_escaped, text_escaped, + text_escaped, text_escaped, + user_fields_str ? user_fields_str : ""); + + book_query = e_book_query_from_string (query_string); + + contact_store = e_name_selector_model_peek_contact_store ( + name_selector_dialog->priv->name_selector_model); + e_contact_store_set_query (contact_store, book_query); + e_book_query_unref (book_query); + + g_free (query_string); + g_free (text_escaped); + g_free (category_escaped); + g_free (category); + g_free (user_fields_str); +} + +static void +contact_selection_changed (ENameSelectorDialog *name_selector_dialog) +{ + GtkTreeSelection *contact_selection; + gboolean have_selection = FALSE; + gint i; + + contact_selection = gtk_tree_view_get_selection ( + name_selector_dialog->priv->contact_view); + if (gtk_tree_selection_count_selected_rows (contact_selection)) + have_selection = TRUE; + + for (i = 0; i < name_selector_dialog->priv->sections->len; i++) { + Section *section = &g_array_index ( + name_selector_dialog->priv->sections, Section, i); + gtk_widget_set_sensitive (GTK_WIDGET (section->transfer_button), have_selection); + } +} + +static void +contact_activated (ENameSelectorDialog *name_selector_dialog, + GtkTreePath *path) +{ + EContactStore *contact_store; + EDestinationStore *destination_store; + EContact *contact; + GtkTreeIter iter; + Section *section; + gint email_n; + + /* When a contact is activated, we transfer it to the first destination on our list */ + + contact_store = e_name_selector_model_peek_contact_store ( + name_selector_dialog->priv->name_selector_model); + + /* If we have no sections, we can't transfer */ + if (name_selector_dialog->priv->sections->len == 0) + return; + + /* Get the contact to be transferred */ + + if (!gtk_tree_model_get_iter ( + GTK_TREE_MODEL (name_selector_dialog->priv->contact_sort), + &iter, path)) + g_assert_not_reached (); + + sort_iter_to_contact_store_iter (name_selector_dialog, &iter, &email_n); + + contact = e_contact_store_get_contact (contact_store, &iter); + if (!contact) { + g_warning ("ENameSelectorDialog could not get selected contact!"); + return; + } + + section = &g_array_index ( + name_selector_dialog->priv->sections, + Section, name_selector_dialog->priv->destination_index); + if (!e_name_selector_model_peek_section ( + name_selector_dialog->priv->name_selector_model, + section->name, NULL, &destination_store)) { + g_warning ("ENameSelectorDialog has a section unknown to the model!"); + return; + } + + add_destination ( + name_selector_dialog->priv->name_selector_model, + destination_store, contact, email_n, + e_contact_store_get_client (contact_store, &iter)); +} + +static void +destination_activated (ENameSelectorDialog *name_selector_dialog, + GtkTreePath *path, + GtkTreeViewColumn *column, + GtkTreeView *tree_view) +{ + gint section_index; + EDestinationStore *destination_store; + EDestination *destination; + Section *section; + GtkTreeIter iter; + + /* When a destination is activated, we remove it from the section */ + + section_index = find_section_by_tree_view ( + name_selector_dialog, tree_view); + if (section_index < 0) { + g_warning ("ENameSelectorDialog got activation from unknown view!"); + return; + } + + section = &g_array_index ( + name_selector_dialog->priv->sections, Section, section_index); + if (!e_name_selector_model_peek_section ( + name_selector_dialog->priv->name_selector_model, + section->name, NULL, &destination_store)) { + g_warning ("ENameSelectorDialog has a section unknown to the model!"); + return; + } + + if (!gtk_tree_model_get_iter ( + GTK_TREE_MODEL (destination_store), &iter, path)) + g_assert_not_reached (); + + destination = e_destination_store_get_destination ( + destination_store, &iter); + g_assert (destination); + + e_destination_store_remove_destination ( + destination_store, destination); +} + +static gboolean +remove_selection (ENameSelectorDialog *name_selector_dialog, + GtkTreeView *tree_view) +{ + gint section_index; + EDestinationStore *destination_store; + EDestination *destination; + Section *section; + GtkTreeSelection *selection; + GList *rows, *l; + + section_index = find_section_by_tree_view ( + name_selector_dialog, tree_view); + if (section_index < 0) { + g_warning ("ENameSelectorDialog got key press from unknown view!"); + return FALSE; + } + + section = &g_array_index ( + name_selector_dialog->priv->sections, Section, section_index); + if (!e_name_selector_model_peek_section ( + name_selector_dialog->priv->name_selector_model, + section->name, NULL, &destination_store)) { + g_warning ("ENameSelectorDialog has a section unknown to the model!"); + return FALSE; + } + + selection = gtk_tree_view_get_selection (tree_view); + if (!gtk_tree_selection_count_selected_rows (selection)) { + g_warning ("ENameSelectorDialog remove button clicked, but no selection!"); + return FALSE; + } + + rows = gtk_tree_selection_get_selected_rows (selection, NULL); + rows = g_list_reverse (rows); + + for (l = rows; l; l = g_list_next (l)) { + GtkTreeIter iter; + GtkTreePath *path = l->data; + + if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (destination_store), + &iter, path)) + g_assert_not_reached (); + + gtk_tree_path_free (path); + + destination = e_destination_store_get_destination ( + destination_store, &iter); + g_assert (destination); + + e_destination_store_remove_destination ( + destination_store, destination); + } + g_list_free (rows); + + return TRUE; +} + +static void +remove_button_clicked (GtkButton *button, + SelData *data) +{ + GtkTreeView *view; + ENameSelectorDialog *name_selector_dialog; + + view = data->view; + name_selector_dialog = data->dlg_ptr; + remove_selection (name_selector_dialog, view); +} + +static gboolean +destination_key_press (ENameSelectorDialog *name_selector_dialog, + GdkEventKey *event, + GtkTreeView *tree_view) +{ + + /* we only care about DEL key */ + if (event->keyval != GDK_KEY_Delete) + return FALSE; + return remove_selection (name_selector_dialog, tree_view); + +} + +static void +transfer_button_clicked (ENameSelectorDialog *name_selector_dialog, + GtkButton *transfer_button) +{ + EContactStore *contact_store; + EDestinationStore *destination_store; + GtkTreeSelection *selection; + EContact *contact; + gint section_index; + Section *section; + gint email_n; + GList *rows, *l; + + /* Get the contact to be transferred */ + + contact_store = e_name_selector_model_peek_contact_store ( + name_selector_dialog->priv->name_selector_model); + selection = gtk_tree_view_get_selection ( + name_selector_dialog->priv->contact_view); + + if (!gtk_tree_selection_count_selected_rows (selection)) { + g_warning ("ENameSelectorDialog transfer button clicked, but no selection!"); + return; + } + + /* Get the target section */ + section_index = find_section_by_transfer_button ( + name_selector_dialog, transfer_button); + if (section_index < 0) { + g_warning ("ENameSelectorDialog got click from unknown button!"); + return; + } + + section = &g_array_index ( + name_selector_dialog->priv->sections, Section, section_index); + if (!e_name_selector_model_peek_section ( + name_selector_dialog->priv->name_selector_model, + section->name, NULL, &destination_store)) { + g_warning ("ENameSelectorDialog has a section unknown to the model!"); + return; + } + + rows = gtk_tree_selection_get_selected_rows (selection, NULL); + rows = g_list_reverse (rows); + + for (l = rows; l; l = g_list_next (l)) { + GtkTreeIter iter; + GtkTreePath *path = l->data; + + if (!gtk_tree_model_get_iter ( + GTK_TREE_MODEL (name_selector_dialog->priv->contact_sort), + &iter, path)) { + gtk_tree_path_free (path); + return; + } + + gtk_tree_path_free (path); + sort_iter_to_contact_store_iter (name_selector_dialog, &iter, &email_n); + + contact = e_contact_store_get_contact (contact_store, &iter); + if (!contact) { + g_warning ("ENameSelectorDialog could not get selected contact!"); + g_list_free (rows); + return; + } + + add_destination ( + name_selector_dialog->priv->name_selector_model, + destination_store, contact, email_n, + e_contact_store_get_client (contact_store, &iter)); + } + g_list_free (rows); +} + +/* --------------------- * + * Main model management * + * --------------------- */ + +static void +setup_name_selector_model (ENameSelectorDialog *name_selector_dialog) +{ + ETreeModelGenerator *contact_filter; + EContactStore *contact_store; + GList *new_sections; + GList *l; + + /* Create new destination sections in UI */ + + new_sections = e_name_selector_model_list_sections ( + name_selector_dialog->priv->name_selector_model); + + for (l = new_sections; l; l = g_list_next (l)) { + gchar *name = l->data; + gchar *pretty_name; + EDestinationStore *destination_store; + + e_name_selector_model_peek_section ( + name_selector_dialog->priv->name_selector_model, + name, &pretty_name, &destination_store); + + add_section (name_selector_dialog, name, pretty_name, destination_store); + + g_free (pretty_name); + g_free (name); + } + + g_list_free (new_sections); + + /* Connect to section add/remove signals */ + + g_signal_connect_swapped ( + name_selector_dialog->priv->name_selector_model, "section-added", + G_CALLBACK (model_section_added), name_selector_dialog); + g_signal_connect_swapped ( + name_selector_dialog->priv->name_selector_model, "section-removed", + G_CALLBACK (model_section_removed), name_selector_dialog); + + /* Get contact store and its filter wrapper */ + + contact_filter = e_name_selector_model_peek_contact_filter ( + name_selector_dialog->priv->name_selector_model); + + /* Create sorting model on top of filter, assign it to view */ + + name_selector_dialog->priv->contact_sort = GTK_TREE_MODEL_SORT ( + gtk_tree_model_sort_new_with_model (GTK_TREE_MODEL (contact_filter))); + + /* sort on full name as we display full name in name selector dialog */ + gtk_tree_sortable_set_sort_column_id ( + GTK_TREE_SORTABLE (name_selector_dialog->priv->contact_sort), + E_CONTACT_FULL_NAME, GTK_SORT_ASCENDING); + + gtk_tree_view_set_model ( + name_selector_dialog->priv->contact_view, + GTK_TREE_MODEL (name_selector_dialog->priv->contact_sort)); + + contact_store = e_name_selector_model_peek_contact_store (name_selector_dialog->priv->name_selector_model); + if (contact_store) { + g_signal_connect (contact_store, "start-client-view", G_CALLBACK (start_client_view_cb), name_selector_dialog); + g_signal_connect (contact_store, "stop-client-view", G_CALLBACK (stop_client_view_cb), name_selector_dialog); + } + + /* Make sure UI is consistent */ + + search_changed (name_selector_dialog); + contact_selection_changed (name_selector_dialog); +} + +static void +shutdown_name_selector_model (ENameSelectorDialog *name_selector_dialog) +{ + gint i; + + /* Rid UI of previous destination sections */ + + for (i = 0; i < name_selector_dialog->priv->sections->len; i++) + free_section (name_selector_dialog, i); + + g_array_set_size (name_selector_dialog->priv->sections, 0); + + /* Free sorting model */ + + if (name_selector_dialog->priv->contact_sort) { + g_object_unref (name_selector_dialog->priv->contact_sort); + name_selector_dialog->priv->contact_sort = NULL; + } + + /* Free backend model */ + + if (name_selector_dialog->priv->name_selector_model) { + EContactStore *contact_store; + + contact_store = e_name_selector_model_peek_contact_store (name_selector_dialog->priv->name_selector_model); + if (contact_store) { + g_signal_handlers_disconnect_by_func (contact_store, start_client_view_cb, name_selector_dialog); + g_signal_handlers_disconnect_by_func (contact_store, stop_client_view_cb, name_selector_dialog); + } + + g_signal_handlers_disconnect_matched ( + name_selector_dialog->priv->name_selector_model, + G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_dialog); + + g_object_unref (name_selector_dialog->priv->name_selector_model); + name_selector_dialog->priv->name_selector_model = NULL; + } +} + +static void +contact_column_formatter (GtkTreeViewColumn *column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + ENameSelectorDialog *name_selector_dialog) +{ + EContactStore *contact_store; + EContact *contact; + GtkTreeIter contact_store_iter; + GList *email_list; + gchar *string; + gchar *full_name_str; + gchar *email_str; + gint email_n; + + contact_store_iter = *iter; + sort_iter_to_contact_store_iter ( + name_selector_dialog, &contact_store_iter, &email_n); + + contact_store = e_name_selector_model_peek_contact_store ( + name_selector_dialog->priv->name_selector_model); + contact = e_contact_store_get_contact ( + contact_store, &contact_store_iter); + email_list = e_name_selector_model_get_contact_emails_without_used ( + name_selector_dialog->priv->name_selector_model, contact, TRUE); + email_str = g_list_nth_data (email_list, email_n); + full_name_str = e_contact_get (contact, E_CONTACT_FULL_NAME); + + if (e_contact_get (contact, E_CONTACT_IS_LIST)) { + if (!full_name_str) + full_name_str = e_contact_get (contact, E_CONTACT_FILE_AS); + string = g_strdup_printf ("%s", full_name_str ? full_name_str : "?"); + } else { + string = g_strdup_printf ( + "%s%s<%s>", full_name_str ? full_name_str : "", + full_name_str ? " " : "", + email_str ? email_str : ""); + } + + g_free (full_name_str); + e_name_selector_model_free_emails_list (email_list); + + g_object_set (cell, "text", string, NULL); + g_free (string); +} + +static void +destination_column_formatter (GtkTreeViewColumn *column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + ENameSelectorDialog *name_selector_dialog) +{ + EDestinationStore *destination_store = E_DESTINATION_STORE (model); + EDestination *destination; + GString *buffer; + + destination = e_destination_store_get_destination (destination_store, iter); + g_assert (destination); + + buffer = g_string_new (e_destination_get_name (destination)); + + if (!e_destination_is_evolution_list (destination)) { + const gchar *email; + + email = e_destination_get_email (destination); + if (email == NULL || *email == '\0') + email = "?"; + g_string_append_printf (buffer, " <%s>", email); + } + + g_object_set (cell, "text", buffer->str, NULL); + g_string_free (buffer, TRUE); +} + +/* ----------------------- * + * ENameSelectorDialog API * + * ----------------------- */ + +/** + * e_name_selector_dialog_peek_model: + * @name_selector_dialog: an #ENameSelectorDialog + * + * Gets the #ENameSelectorModel used by @name_selector_model. + * + * Returns: The #ENameSelectorModel being used. + **/ +ENameSelectorModel * +e_name_selector_dialog_peek_model (ENameSelectorDialog *name_selector_dialog) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog), NULL); + + return name_selector_dialog->priv->name_selector_model; +} + +/** + * e_name_selector_dialog_set_model: + * @name_selector_dialog: an #ENameSelectorDialog + * @model: an #ENameSelectorModel + * + * Sets the model being used by @name_selector_dialog to @model. + **/ +void +e_name_selector_dialog_set_model (ENameSelectorDialog *name_selector_dialog, + ENameSelectorModel *model) +{ + g_return_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog)); + g_return_if_fail (E_IS_NAME_SELECTOR_MODEL (model)); + + if (model == name_selector_dialog->priv->name_selector_model) + return; + + shutdown_name_selector_model (name_selector_dialog); + name_selector_dialog->priv->name_selector_model = g_object_ref (model); + + setup_name_selector_model (name_selector_dialog); +} + +/** + * e_name_selector_dialog_set_destination_index: + * @name_selector_dialog: an #ENameSelectorDialog + * @index: index of the destination section, starting from 0. + * + * Sets the index number of the destination section. + **/ +void +e_name_selector_dialog_set_destination_index (ENameSelectorDialog *name_selector_dialog, + guint index) +{ + g_return_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog)); + + if (index >= name_selector_dialog->priv->sections->len) + return; + + name_selector_dialog->priv->destination_index = index; +} + +/** + * e_name_selector_dialog_set_scrolling_policy: + * @name_selector_dialog: an #ENameSelectorDialog + * @hscrollbar_policy: scrolling policy for horizontal bar of the contacts window. + * @vscrollbar_policy: scrolling policy for vertical bar of the contacts window. + * + * Sets the scrolling policy for the contacts section. + * + * Since: 3.2 + **/ +void +e_name_selector_dialog_set_scrolling_policy (ENameSelectorDialog *name_selector_dialog, + GtkPolicyType hscrollbar_policy, + GtkPolicyType vscrollbar_policy) +{ + GtkScrolledWindow *win = GTK_SCROLLED_WINDOW (name_selector_dialog->priv->contact_window); + + gtk_scrolled_window_set_policy (win, hscrollbar_policy, vscrollbar_policy); +} + +/** + * e_name_selector_dialog_get_section_visible: + * @name_selector_dialog: an #ENameSelectorDialog + * @name: name of the section + * + * Returns: whether section named @name is visible in the dialog. + * + * Since: 3.8 + **/ +gboolean +e_name_selector_dialog_get_section_visible (ENameSelectorDialog *name_selector_dialog, + const gchar *name) +{ + Section *section; + gint index; + + g_return_val_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog), FALSE); + g_return_val_if_fail (name != NULL, FALSE); + + index = find_section_by_name (name_selector_dialog, name); + g_return_val_if_fail (index != -1, FALSE); + + section = &g_array_index (name_selector_dialog->priv->sections, Section, index); + return gtk_widget_get_visible (GTK_WIDGET (section->section_grid)); +} + +/** + * e_name_selector_dialog_set_section_visible: + * @name_selector_dialog: an #ENameSelectorDialog + * @name: name of the section + * @visible: whether to show or hide the section + * + * Shows or hides section named @name in the dialog. + * + * Since: 3.8 + **/ +void +e_name_selector_dialog_set_section_visible (ENameSelectorDialog *name_selector_dialog, + const gchar *name, + gboolean visible) +{ + Section *section; + gint index; + + g_return_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog)); + g_return_if_fail (name != NULL); + + index = find_section_by_name (name_selector_dialog, name); + g_return_if_fail (index != -1); + + section = &g_array_index (name_selector_dialog->priv->sections, Section, index); + + if (visible) + gtk_widget_show (GTK_WIDGET (section->section_grid)); + else + gtk_widget_hide (GTK_WIDGET (section->section_grid)); +} + diff --git a/e-util/e-name-selector-dialog.h b/e-util/e-name-selector-dialog.h new file mode 100644 index 0000000000..fe10544bb5 --- /dev/null +++ b/e-util/e-name-selector-dialog.h @@ -0,0 +1,100 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-name-selector-dialog.c - Dialog that lets user pick EDestinations. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_NAME_SELECTOR_DIALOG_H +#define E_NAME_SELECTOR_DIALOG_H + +#include <gtk/gtk.h> +#include <libedataserver/libedataserver.h> + +#include <e-util/e-contact-store.h> +#include <e-util/e-name-selector-model.h> + +/* Standard GObject macros */ +#define E_TYPE_NAME_SELECTOR_DIALOG \ + (e_name_selector_dialog_get_type ()) +#define E_NAME_SELECTOR_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_NAME_SELECTOR_DIALOG, ENameSelectorDialog)) +#define E_NAME_SELECTOR_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_NAME_SELECTOR_DIALOG, ENameSelectorDialogClass)) +#define E_IS_NAME_SELECTOR_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + (obj, E_TYPE_NAME_SELECTOR_DIALOG)) +#define E_IS_NAME_SELECTOR_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_NAME_SELECTOR_DIALOG)) +#define E_NAME_SELECTOR_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_NAME_SELECTOR_DIALOG, ENameSelectorDialogClass)) + +G_BEGIN_DECLS + +typedef struct _ENameSelectorDialog ENameSelectorDialog; +typedef struct _ENameSelectorDialogClass ENameSelectorDialogClass; +typedef struct _ENameSelectorDialogPrivate ENameSelectorDialogPrivate; + +struct _ENameSelectorDialog { + GtkDialog parent; + ENameSelectorDialogPrivate *priv; +}; + +struct _ENameSelectorDialogClass { + GtkDialogClass parent_class; +}; + +GType e_name_selector_dialog_get_type (void); +ENameSelectorDialog * + e_name_selector_dialog_new (ESourceRegistry *registry); +ESourceRegistry * + e_name_selector_dialog_get_registry + (ENameSelectorDialog *name_selector_dialog); +ENameSelectorModel * + e_name_selector_dialog_peek_model + (ENameSelectorDialog *name_selector_dialog); +void e_name_selector_dialog_set_model + (ENameSelectorDialog *name_selector_dialog, + ENameSelectorModel *model); +void e_name_selector_dialog_set_destination_index + (ENameSelectorDialog *name_selector_dialog, + guint index); +void e_name_selector_dialog_set_scrolling_policy + (ENameSelectorDialog *name_selector_dialog, + GtkPolicyType hscrollbar_policy, + GtkPolicyType vscrollbar_policy); +gboolean e_name_selector_dialog_get_section_visible + (ENameSelectorDialog *name_selector_dialog, + const gchar *name); +void e_name_selector_dialog_set_section_visible + (ENameSelectorDialog *name_selector_dialog, + const gchar *name, + gboolean visible); + +G_END_DECLS + +#endif /* E_NAME_SELECTOR_DIALOG_H */ diff --git a/e-util/e-name-selector-entry.c b/e-util/e-name-selector-entry.c new file mode 100644 index 0000000000..ea7e2ef383 --- /dev/null +++ b/e-util/e-name-selector-entry.c @@ -0,0 +1,3541 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-name-selector-entry.c - Single-line text entry widget for EDestinations. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#include <config.h> +#include <string.h> +#include <glib/gi18n-lib.h> + +#include <camel/camel.h> +#include <libebackend/libebackend.h> + +#include "e-client-utils.h" +#include "e-name-selector-entry.h" + +#define E_NAME_SELECTOR_ENTRY_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_NAME_SELECTOR_ENTRY, ENameSelectorEntryPrivate)) + +struct _ENameSelectorEntryPrivate { + + ESourceRegistry *registry; + gint minimum_query_length; + gboolean show_address; + + PangoAttrList *attr_list; + EContactStore *contact_store; + ETreeModelGenerator *email_generator; + EDestinationStore *destination_store; + GtkEntryCompletion *entry_completion; + + guint type_ahead_complete_cb_id; + guint update_completions_cb_id; + + EDestination *popup_destination; + + gpointer (*contact_editor_func) (EBookClient *, + EContact *, + gboolean, + gboolean); + gpointer (*contact_list_editor_func) + (EBookClient *, + EContact *, + gboolean, + gboolean); + + gboolean is_completing; + GSList *user_query_fields; + + /* For asynchronous operations. */ + GQueue cancellables; +}; + +enum { + PROP_0, + PROP_REGISTRY, + PROP_MINIMUM_QUERY_LENGTH, + PROP_SHOW_ADDRESS +}; + +enum { + UPDATED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; +#define ENS_DEBUG(x) + +G_DEFINE_TYPE_WITH_CODE ( + ENameSelectorEntry, + e_name_selector_entry, + GTK_TYPE_ENTRY, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +/* 1/3 of the second to wait until invoking autocomplete lookup */ +#define AUTOCOMPLETE_TIMEOUT 333 + +#define re_set_timeout(id,func,ptr) \ + if (id) \ + g_source_remove (id); \ + id = g_timeout_add (AUTOCOMPLETE_TIMEOUT, \ + (GSourceFunc) func, ptr); + +static void destination_row_inserted (ENameSelectorEntry *name_selector_entry, GtkTreePath *path, GtkTreeIter *iter); +static void destination_row_changed (ENameSelectorEntry *name_selector_entry, GtkTreePath *path, GtkTreeIter *iter); +static void destination_row_deleted (ENameSelectorEntry *name_selector_entry, GtkTreePath *path); + +static void user_insert_text (ENameSelectorEntry *name_selector_entry, gchar *new_text, gint new_text_length, gint *position, gpointer user_data); +static void user_delete_text (ENameSelectorEntry *name_selector_entry, gint start_pos, gint end_pos, gpointer user_data); + +static void setup_default_contact_store (ENameSelectorEntry *name_selector_entry); +static void deep_free_list (GList *list); + +static void +name_selector_entry_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + e_name_selector_entry_set_registry ( + E_NAME_SELECTOR_ENTRY (object), + g_value_get_object (value)); + return; + + case PROP_MINIMUM_QUERY_LENGTH: + e_name_selector_entry_set_minimum_query_length ( + E_NAME_SELECTOR_ENTRY (object), + g_value_get_int (value)); + return; + + case PROP_SHOW_ADDRESS: + e_name_selector_entry_set_show_address ( + E_NAME_SELECTOR_ENTRY (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +name_selector_entry_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + g_value_set_object ( + value, + e_name_selector_entry_get_registry ( + E_NAME_SELECTOR_ENTRY (object))); + return; + + case PROP_MINIMUM_QUERY_LENGTH: + g_value_set_int ( + value, + e_name_selector_entry_get_minimum_query_length ( + E_NAME_SELECTOR_ENTRY (object))); + return; + + case PROP_SHOW_ADDRESS: + g_value_set_boolean ( + value, + e_name_selector_entry_get_show_address ( + E_NAME_SELECTOR_ENTRY (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +name_selector_entry_dispose (GObject *object) +{ + ENameSelectorEntryPrivate *priv; + + priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->attr_list != NULL) { + pango_attr_list_unref (priv->attr_list); + priv->attr_list = NULL; + } + + if (priv->entry_completion) { + g_object_unref (priv->entry_completion); + priv->entry_completion = NULL; + } + + if (priv->destination_store) { + g_object_unref (priv->destination_store); + priv->destination_store = NULL; + } + + if (priv->email_generator) { + g_object_unref (priv->email_generator); + priv->email_generator = NULL; + } + + if (priv->contact_store) { + g_object_unref (priv->contact_store); + priv->contact_store = NULL; + } + + g_slist_foreach (priv->user_query_fields, (GFunc) g_free, NULL); + g_slist_free (priv->user_query_fields); + priv->user_query_fields = NULL; + + /* Cancel any stuck book loading operations. */ + while (!g_queue_is_empty (&priv->cancellables)) { + GCancellable *cancellable; + + cancellable = g_queue_pop_head (&priv->cancellables); + g_cancellable_cancel (cancellable); + g_object_unref (cancellable); + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_name_selector_entry_parent_class)->dispose (object); +} + +static void +name_selector_entry_constructed (GObject *object) +{ + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_name_selector_entry_parent_class)-> + constructed (object); + + e_extensible_load_extensions (E_EXTENSIBLE (object)); +} + +static void +name_selector_entry_realize (GtkWidget *widget) +{ + ENameSelectorEntryPrivate *priv; + + priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (widget); + + /* Chain up to parent's realize() method. */ + GTK_WIDGET_CLASS (e_name_selector_entry_parent_class)->realize (widget); + + if (priv->contact_store == NULL) + setup_default_contact_store (E_NAME_SELECTOR_ENTRY (widget)); +} + +static void +name_selector_entry_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time) +{ + CamelInternetAddress *address; + gint n_addresses = 0; + gchar *text; + + address = camel_internet_address_new (); + text = (gchar *) gtk_selection_data_get_text (selection_data); + + /* See if Camel can parse a valid email address from the text. */ + if (text != NULL && *text != '\0') { + camel_url_decode (text); + if (g_ascii_strncasecmp (text, "mailto:", 7) == 0) + n_addresses = camel_address_decode ( + CAMEL_ADDRESS (address), text + 7); + else + n_addresses = camel_address_decode ( + CAMEL_ADDRESS (address), text); + } + + if (n_addresses > 0) { + GtkEditable *editable; + GdkDragAction action; + gboolean delete; + gint position; + + editable = GTK_EDITABLE (widget); + gtk_editable_set_position (editable, -1); + position = gtk_editable_get_position (editable); + + g_free (text); + + text = camel_address_format (CAMEL_ADDRESS (address)); + gtk_editable_insert_text (editable, text, -1, &position); + + action = gdk_drag_context_get_selected_action (context); + delete = (action == GDK_ACTION_MOVE); + gtk_drag_finish (context, TRUE, delete, time); + } + + g_object_unref (address); + g_free (text); + + if (n_addresses <= 0) + /* Chain up to parent's drag_data_received() method. */ + GTK_WIDGET_CLASS (e_name_selector_entry_parent_class)-> + drag_data_received ( + widget, context, x, y, + selection_data, info, time); +} + +static void +e_name_selector_entry_class_init (ENameSelectorEntryClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (ENameSelectorEntryPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = name_selector_entry_set_property; + object_class->get_property = name_selector_entry_get_property; + object_class->dispose = name_selector_entry_dispose; + object_class->constructed = name_selector_entry_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->realize = name_selector_entry_realize; + widget_class->drag_data_received = name_selector_entry_drag_data_received; + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + "Data source registry", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_MINIMUM_QUERY_LENGTH, + g_param_spec_int ( + "minimum-query-length", + "Minimum Query Length", + NULL, + 1, G_MAXINT, + 3, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_ADDRESS, + g_param_spec_boolean ( + "show-address", + "Show Address", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + signals[UPDATED] = g_signal_new ( + "updated", + E_TYPE_NAME_SELECTOR_ENTRY, + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (ENameSelectorEntryClass, updated), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, G_TYPE_POINTER); +} + +/* Remove unquoted commas and control characters from string */ +static gchar * +sanitize_string (const gchar *string) +{ + GString *gstring; + gboolean quoted = FALSE; + const gchar *p; + + gstring = g_string_new (""); + + if (!string) + return g_string_free (gstring, FALSE); + + for (p = string; *p; p = g_utf8_next_char (p)) { + gunichar c = g_utf8_get_char (p); + + if (c == '"') + quoted = ~quoted; + else if (c == ',' && !quoted) + continue; + else if (c == '\t' || c == '\n') + continue; + + g_string_append_unichar (gstring, c); + } + + return g_string_free (gstring, FALSE); +} + +/* Called for each list store entry whenever the user types (but not on cut/paste) */ +static gboolean +completion_match_cb (GtkEntryCompletion *completion, + const gchar *key, + GtkTreeIter *iter, + gpointer user_data) +{ + ENS_DEBUG (g_print ("completion_match_cb, key=%s\n", key)); + + return TRUE; +} + +/* Gets context of n_unichars total (n_unicars / 2, before and after position) + * and places them in array. If any positions would be outside the string, the + * corresponding unichars are set to zero. */ +static void +get_utf8_string_context (const gchar *string, + gint position, + gunichar *unichars, + gint n_unichars) +{ + gchar *p = NULL; + gint len; + gint gap; + gint i; + + /* n_unichars must be even */ + g_assert (n_unichars % 2 == 0); + + len = g_utf8_strlen (string, -1); + gap = n_unichars / 2; + + for (i = 0; i < n_unichars; i++) { + gint char_pos = position - gap + i; + + if (char_pos < 0 || char_pos >= len) { + unichars[i] = '\0'; + continue; + } + + if (p) + p = g_utf8_next_char (p); + else + p = g_utf8_offset_to_pointer (string, char_pos); + + unichars[i] = g_utf8_get_char (p); + } +} + +static gboolean +get_range_at_position (const gchar *string, + gint pos, + gint *start_pos, + gint *end_pos) +{ + const gchar *p; + gboolean quoted = FALSE; + gint local_start_pos = 0; + gint local_end_pos = 0; + gint i; + + if (!string || !*string) + return FALSE; + + for (p = string, i = 0; *p; p = g_utf8_next_char (p), i++) { + gunichar c = g_utf8_get_char (p); + + if (c == '"') { + quoted = ~quoted; + } else if (c == ',' && !quoted) { + if (i < pos) { + /* Start right after comma */ + local_start_pos = i + 1; + } else { + /* Stop right before comma */ + local_end_pos = i; + break; + } + } else if (c == ' ' && local_start_pos == i) { + /* Adjust start to skip space after first comma */ + local_start_pos++; + } + } + + /* If we didn't hit a comma, we must've hit NULL, and ours was the last element. */ + if (!local_end_pos) + local_end_pos = i; + + if (start_pos) + *start_pos = local_start_pos; + if (end_pos) + *end_pos = local_end_pos; + + return TRUE; +} + +static gboolean +is_quoted_at (const gchar *string, + gint pos) +{ + const gchar *p; + gboolean quoted = FALSE; + gint i; + + for (p = string, i = 0; *p && i < pos; p = g_utf8_next_char (p), i++) { + gunichar c = g_utf8_get_char (p); + + if (c == '"') + quoted = ~quoted; + } + + return quoted ? TRUE : FALSE; +} + +static gint +get_index_at_position (const gchar *string, + gint pos) +{ + const gchar *p; + gboolean quoted = FALSE; + gint n = 0; + gint i; + + for (p = string, i = 0; *p && i < pos; p = g_utf8_next_char (p), i++) { + gunichar c = g_utf8_get_char (p); + + if (c == '"') + quoted = ~quoted; + else if (c == ',' && !quoted) + n++; + } + + return n; +} + +static gboolean +get_range_by_index (const gchar *string, + gint index, + gint *start_pos, + gint *end_pos) +{ + const gchar *p; + gboolean quoted = FALSE; + gint i; + gint n = 0; + + for (p = string, i = 0; *p && n < index; p = g_utf8_next_char (p), i++) { + gunichar c = g_utf8_get_char (p); + + if (c == '"') + quoted = ~quoted; + if (c == ',' && !quoted) + n++; + } + + if (n < index) + return FALSE; + + return get_range_at_position (string, i, start_pos, end_pos); +} + +static gchar * +get_address_at_position (const gchar *string, + gint pos) +{ + gint start_pos; + gint end_pos; + const gchar *start_p; + const gchar *end_p; + + if (!get_range_at_position (string, pos, &start_pos, &end_pos)) + return NULL; + + start_p = g_utf8_offset_to_pointer (string, start_pos); + end_p = g_utf8_offset_to_pointer (string, end_pos); + + return g_strndup (start_p, end_p - start_p); +} + +/* Finds the destination in model */ +static EDestination * +find_destination_by_index (ENameSelectorEntry *name_selector_entry, + gint index) +{ + GtkTreePath *path; + GtkTreeIter iter; + + path = gtk_tree_path_new_from_indices (index, -1); + if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (name_selector_entry->priv->destination_store), + &iter, path)) { + /* If we have zero destinations, getting a NULL destination at index 0 + * is valid. */ + if (index > 0) + g_warning ("ENameSelectorEntry is out of sync with model!"); + gtk_tree_path_free (path); + return NULL; + } + gtk_tree_path_free (path); + + return e_destination_store_get_destination (name_selector_entry->priv->destination_store, &iter); +} + +/* Finds the destination in model */ +static EDestination * +find_destination_at_position (ENameSelectorEntry *name_selector_entry, + gint pos) +{ + const gchar *text; + gint index; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + index = get_index_at_position (text, pos); + + return find_destination_by_index (name_selector_entry, index); +} + +/* Builds destination from our text */ +static EDestination * +build_destination_at_position (const gchar *string, + gint pos) +{ + EDestination *destination; + gchar *address; + + address = get_address_at_position (string, pos); + if (!address) + return NULL; + + destination = e_destination_new (); + e_destination_set_raw (destination, address); + + g_free (address); + return destination; +} + +static gchar * +name_style_query (const gchar *field, + const gchar *value) +{ + gchar *spaced_str; + gchar *comma_str; + GString *out = g_string_new (""); + gchar **strv; + gchar *query; + + spaced_str = sanitize_string (value); + g_strstrip (spaced_str); + + strv = g_strsplit (spaced_str, " ", 0); + + if (strv[0] && strv[1]) { + g_string_append (out, "(or "); + comma_str = g_strjoinv (", ", strv); + } else { + comma_str = NULL; + } + + g_string_append (out, " (beginswith "); + e_sexp_encode_string (out, field); + e_sexp_encode_string (out, spaced_str); + g_string_append (out, ")"); + + if (comma_str) { + g_string_append (out, " (beginswith "); + + e_sexp_encode_string (out, field); + g_strstrip (comma_str); + e_sexp_encode_string (out, comma_str); + g_string_append (out, "))"); + } + + query = g_string_free (out, FALSE); + + g_free (spaced_str); + g_free (comma_str); + g_strfreev (strv); + + return query; +} + +static gchar * +escape_sexp_string (const gchar *string) +{ + GString *gstring; + gchar *encoded_string; + + gstring = g_string_new (""); + e_sexp_encode_string (gstring, string); + + encoded_string = gstring->str; + g_string_free (gstring, FALSE); + + return encoded_string; +} + +/** + * ens_util_populate_user_query_fields: + * + * Populates list of user query fields to string usable in query string. + * Returned pointer is either newly allocated string, supposed to be freed with g_free, + * or NULL if no fields defined. + * + * Since: 2.24 + **/ +gchar * +ens_util_populate_user_query_fields (GSList *user_query_fields, + const gchar *cue_str, + const gchar *encoded_cue_str) +{ + GString *user_fields; + GSList *s; + + g_return_val_if_fail (cue_str != NULL, NULL); + g_return_val_if_fail (encoded_cue_str != NULL, NULL); + + user_fields = g_string_new (""); + + for (s = user_query_fields; s; s = s->next) { + const gchar *field = s->data; + + if (!field || !*field) + continue; + + if (*field == '$') { + g_string_append_printf (user_fields, " (beginswith \"%s\" %s) ", field + 1, encoded_cue_str); + } else if (*field == '@') { + g_string_append_printf (user_fields, " (is \"%s\" %s) ", field + 1, encoded_cue_str); + } else { + gchar *tmp = name_style_query (field, cue_str); + + g_string_append (user_fields, " "); + g_string_append (user_fields, tmp); + g_string_append (user_fields, " "); + g_free (tmp); + } + } + + return g_string_free (user_fields, !user_fields->str || !*user_fields->str); +} + +static void +set_completion_query (ENameSelectorEntry *name_selector_entry, + const gchar *cue_str) +{ + ENameSelectorEntryPrivate *priv; + EBookQuery *book_query; + gchar *query_str; + gchar *encoded_cue_str; + gchar *full_name_query_str; + gchar *file_as_query_str; + gchar *user_fields_str; + + priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry); + + if (!name_selector_entry->priv->contact_store) + return; + + if (!cue_str) { + /* Clear the store */ + e_contact_store_set_query (name_selector_entry->priv->contact_store, NULL); + return; + } + + encoded_cue_str = escape_sexp_string (cue_str); + full_name_query_str = name_style_query ("full_name", cue_str); + file_as_query_str = name_style_query ("file_as", cue_str); + user_fields_str = ens_util_populate_user_query_fields (priv->user_query_fields, cue_str, encoded_cue_str); + + query_str = g_strdup_printf ( + "(or " + " (beginswith \"nickname\" %s) " + " (beginswith \"email\" %s) " + " %s " + " %s " + " %s " + ")", + encoded_cue_str, encoded_cue_str, + full_name_query_str, file_as_query_str, + user_fields_str ? user_fields_str : ""); + + g_free (user_fields_str); + g_free (file_as_query_str); + g_free (full_name_query_str); + g_free (encoded_cue_str); + + ENS_DEBUG (g_print ("%s\n", query_str)); + + book_query = e_book_query_from_string (query_str); + e_contact_store_set_query (name_selector_entry->priv->contact_store, book_query); + e_book_query_unref (book_query); + + g_free (query_str); +} + +static gchar * +get_entry_substring (ENameSelectorEntry *name_selector_entry, + gint range_start, + gint range_end) +{ + const gchar *entry_text; + gchar *p0, *p1; + + entry_text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + + p0 = g_utf8_offset_to_pointer (entry_text, range_start); + p1 = g_utf8_offset_to_pointer (entry_text, range_end); + + return g_strndup (p0, p1 - p0); +} + +static gint +utf8_casefold_collate_len (const gchar *str1, + const gchar *str2, + gint len) +{ + gchar *s1 = g_utf8_casefold (str1, len); + gchar *s2 = g_utf8_casefold (str2, len); + gint rv; + + rv = g_utf8_collate (s1, s2); + + g_free (s1); + g_free (s2); + + return rv; +} + +static gchar * +build_textrep_for_contact (EContact *contact, + EContactField cue_field) +{ + gchar *name = NULL; + gchar *email = NULL; + gchar *textrep; + + switch (cue_field) { + case E_CONTACT_FULL_NAME: + case E_CONTACT_NICKNAME: + case E_CONTACT_FILE_AS: + name = e_contact_get (contact, cue_field); + email = e_contact_get (contact, E_CONTACT_EMAIL_1); + break; + + case E_CONTACT_EMAIL_1: + case E_CONTACT_EMAIL_2: + case E_CONTACT_EMAIL_3: + case E_CONTACT_EMAIL_4: + name = NULL; + email = e_contact_get (contact, cue_field); + break; + + default: + g_assert_not_reached (); + break; + } + + g_assert (email); + g_assert (strlen (email) > 0); + + if (name) + textrep = g_strdup_printf ("%s <%s>", name, email); + else + textrep = g_strdup_printf ("%s", email); + + g_free (name); + g_free (email); + return textrep; +} + +static gboolean +contact_match_cue (ENameSelectorEntry *name_selector_entry, + EContact *contact, + const gchar *cue_str, + EContactField *matched_field, + gint *matched_field_rank) +{ + EContactField fields[] = { E_CONTACT_FULL_NAME, E_CONTACT_NICKNAME, E_CONTACT_FILE_AS, + E_CONTACT_EMAIL_1, E_CONTACT_EMAIL_2, E_CONTACT_EMAIL_3, + E_CONTACT_EMAIL_4 }; + gchar *email; + gboolean result = FALSE; + gint cue_len; + gint i; + + g_assert (contact); + g_assert (cue_str); + + if (g_utf8_strlen (cue_str, -1) < name_selector_entry->priv->minimum_query_length) + return FALSE; + + cue_len = strlen (cue_str); + + /* Make sure contact has an email address */ + email = e_contact_get (contact, E_CONTACT_EMAIL_1); + if (!email || !*email) { + g_free (email); + return FALSE; + } + g_free (email); + + for (i = 0; i < G_N_ELEMENTS (fields); i++) { + gchar *value; + gchar *value_sane; + + /* Don't match e-mail addresses in contact lists */ + if (e_contact_get (contact, E_CONTACT_IS_LIST) && + fields[i] >= E_CONTACT_FIRST_EMAIL_ID && + fields[i] <= E_CONTACT_LAST_EMAIL_ID) + continue; + + value = e_contact_get (contact, fields[i]); + if (!value) + continue; + + value_sane = sanitize_string (value); + g_free (value); + + ENS_DEBUG (g_print ("Comparing '%s' to '%s'\n", value, cue_str)); + + if (!utf8_casefold_collate_len (value_sane, cue_str, cue_len)) { + if (matched_field) + *matched_field = fields [i]; + if (matched_field_rank) + *matched_field_rank = i; + + result = TRUE; + g_free (value_sane); + break; + } + g_free (value_sane); + } + + return result; +} + +static gboolean +find_existing_completion (ENameSelectorEntry *name_selector_entry, + const gchar *cue_str, + EContact **contact, + gchar **text, + EContactField *matched_field, + EBookClient **book_client) +{ + GtkTreeIter iter; + EContact *best_contact = NULL; + gint best_field_rank = G_MAXINT; + EContactField best_field = 0; + EBookClient *best_book_client = NULL; + + g_assert (cue_str); + + if (!name_selector_entry->priv->contact_store) + return FALSE; + + ENS_DEBUG (g_print ("Completing '%s'\n", cue_str)); + + if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (name_selector_entry->priv->contact_store), &iter)) + return FALSE; + + do { + EContact *current_contact; + gint current_field_rank; + EContactField current_field; + gboolean matches; + + current_contact = e_contact_store_get_contact (name_selector_entry->priv->contact_store, &iter); + if (!current_contact) + continue; + + matches = contact_match_cue (name_selector_entry, current_contact, cue_str, ¤t_field, ¤t_field_rank); + if (matches && current_field_rank < best_field_rank) { + best_contact = current_contact; + best_field_rank = current_field_rank; + best_field = current_field; + best_book_client = e_contact_store_get_client (name_selector_entry->priv->contact_store, &iter); + } + } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (name_selector_entry->priv->contact_store), &iter)); + + if (!best_contact) + return FALSE; + + if (contact) + *contact = best_contact; + if (text) + *text = build_textrep_for_contact (best_contact, best_field); + if (matched_field) + *matched_field = best_field; + if (book_client) + *book_client = best_book_client; + + return TRUE; +} + +static void +generate_attribute_list (ENameSelectorEntry *name_selector_entry) +{ + PangoLayout *layout; + PangoAttrList *attr_list; + const gchar *text; + gint i; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + layout = gtk_entry_get_layout (GTK_ENTRY (name_selector_entry)); + + /* Set up the attribute list */ + + attr_list = pango_attr_list_new (); + + if (name_selector_entry->priv->attr_list) + pango_attr_list_unref (name_selector_entry->priv->attr_list); + + name_selector_entry->priv->attr_list = attr_list; + + /* Parse the entry's text and apply attributes to real contacts */ + + for (i = 0; ; i++) { + EDestination *destination; + PangoAttribute *attr; + gint start_pos; + gint end_pos; + + if (!get_range_by_index (text, i, &start_pos, &end_pos)) + break; + + destination = find_destination_at_position (name_selector_entry, start_pos); + + /* Destination will be NULL if we have no entries */ + if (!destination || !e_destination_get_contact (destination)) + continue; + + attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE); + attr->start_index = g_utf8_offset_to_pointer (text, start_pos) - text; + attr->end_index = g_utf8_offset_to_pointer (text, end_pos) - text; + pango_attr_list_insert (attr_list, attr); + } + + pango_layout_set_attributes (layout, attr_list); +} + +static gboolean +draw_event (ENameSelectorEntry *name_selector_entry) +{ + PangoLayout *layout; + + layout = gtk_entry_get_layout (GTK_ENTRY (name_selector_entry)); + pango_layout_set_attributes (layout, name_selector_entry->priv->attr_list); + + return FALSE; +} + +static void +type_ahead_complete (ENameSelectorEntry *name_selector_entry) +{ + EContact *contact; + EBookClient *book_client = NULL; + EContactField matched_field; + EDestination *destination; + gint cursor_pos; + gint range_start = 0; + gint range_end = 0; + gint pos = 0; + gchar *textrep; + gint textrep_len; + gint range_len; + const gchar *text; + gchar *cue_str; + gchar *temp_str; + ENameSelectorEntryPrivate *priv; + + priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry); + + cursor_pos = gtk_editable_get_position (GTK_EDITABLE (name_selector_entry)); + if (cursor_pos < 0) + return; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + get_range_at_position (text, cursor_pos, &range_start, &range_end); + range_len = range_end - range_start; + if (range_len < priv->minimum_query_length) + return; + + destination = find_destination_at_position (name_selector_entry, cursor_pos); + + cue_str = get_entry_substring (name_selector_entry, range_start, range_end); + if (!find_existing_completion (name_selector_entry, cue_str, &contact, + &textrep, &matched_field, &book_client)) { + g_free (cue_str); + return; + } + + temp_str = sanitize_string (textrep); + g_free (textrep); + textrep = temp_str; + + textrep_len = g_utf8_strlen (textrep, -1); + pos = range_start; + + g_signal_handlers_block_by_func ( + name_selector_entry, + user_insert_text, name_selector_entry); + g_signal_handlers_block_by_func ( + name_selector_entry, + user_delete_text, name_selector_entry); + g_signal_handlers_block_by_func ( + name_selector_entry->priv->destination_store, + destination_row_changed, name_selector_entry); + + if (textrep_len > range_len) { + gint i; + + /* keep character's case as user types */ + for (i = 0; textrep[i] && cue_str[i]; i++) + textrep[i] = cue_str[i]; + + gtk_editable_delete_text ( + GTK_EDITABLE (name_selector_entry), + range_start, range_end); + gtk_editable_insert_text ( + GTK_EDITABLE (name_selector_entry), + textrep, -1, &pos); + gtk_editable_select_region ( + GTK_EDITABLE (name_selector_entry), + range_end, range_start + textrep_len); + priv->is_completing = TRUE; + } + g_free (cue_str); + + if (contact && destination) { + gint email_n = 0; + + if (matched_field >= E_CONTACT_FIRST_EMAIL_ID && matched_field <= E_CONTACT_LAST_EMAIL_ID) + email_n = matched_field - E_CONTACT_FIRST_EMAIL_ID; + + e_destination_set_contact (destination, contact, email_n); + if (book_client) + e_destination_set_client (destination, book_client); + generate_attribute_list (name_selector_entry); + } + + g_signal_handlers_unblock_by_func ( + name_selector_entry->priv->destination_store, + destination_row_changed, name_selector_entry); + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); + + g_free (textrep); +} + +static void +clear_completion_model (ENameSelectorEntry *name_selector_entry) +{ + ENameSelectorEntryPrivate *priv; + + priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry); + + if (!name_selector_entry->priv->contact_store) + return; + + e_contact_store_set_query (name_selector_entry->priv->contact_store, NULL); + priv->is_completing = FALSE; +} + +static void +update_completion_model (ENameSelectorEntry *name_selector_entry) +{ + const gchar *text; + gint cursor_pos; + gint range_start = 0; + gint range_end = 0; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + cursor_pos = gtk_editable_get_position (GTK_EDITABLE (name_selector_entry)); + + if (cursor_pos >= 0) + get_range_at_position (text, cursor_pos, &range_start, &range_end); + + if (range_end - range_start >= name_selector_entry->priv->minimum_query_length && cursor_pos == range_end) { + gchar *cue_str; + + cue_str = get_entry_substring (name_selector_entry, range_start, range_end); + set_completion_query (name_selector_entry, cue_str); + g_free (cue_str); + } else { + /* N/A; Clear completion model */ + clear_completion_model (name_selector_entry); + } +} + +static gboolean +type_ahead_complete_on_timeout_cb (ENameSelectorEntry *name_selector_entry) +{ + type_ahead_complete (name_selector_entry); + name_selector_entry->priv->type_ahead_complete_cb_id = 0; + return FALSE; +} + +static gboolean +update_completions_on_timeout_cb (ENameSelectorEntry *name_selector_entry) +{ + update_completion_model (name_selector_entry); + name_selector_entry->priv->update_completions_cb_id = 0; + return FALSE; +} + +static void +insert_destination_at_position (ENameSelectorEntry *name_selector_entry, + gint pos) +{ + EDestination *destination; + const gchar *text; + gint index; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + index = get_index_at_position (text, pos); + + destination = build_destination_at_position (text, pos); + g_assert (destination); + + g_signal_handlers_block_by_func ( + name_selector_entry->priv->destination_store, + destination_row_inserted, name_selector_entry); + e_destination_store_insert_destination ( + name_selector_entry->priv->destination_store, + index, destination); + g_signal_handlers_unblock_by_func ( + name_selector_entry->priv->destination_store, + destination_row_inserted, name_selector_entry); + g_object_unref (destination); +} + +static void +modify_destination_at_position (ENameSelectorEntry *name_selector_entry, + gint pos) +{ + EDestination *destination; + const gchar *text; + gchar *raw_address; + gboolean rebuild_attributes = FALSE; + + destination = find_destination_at_position (name_selector_entry, pos); + if (!destination) + return; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + raw_address = get_address_at_position (text, pos); + g_assert (raw_address); + + if (e_destination_get_contact (destination)) + rebuild_attributes = TRUE; + + g_signal_handlers_block_by_func ( + name_selector_entry->priv->destination_store, + destination_row_changed, name_selector_entry); + e_destination_set_raw (destination, raw_address); + g_signal_handlers_unblock_by_func ( + name_selector_entry->priv->destination_store, + destination_row_changed, name_selector_entry); + + g_free (raw_address); + + if (rebuild_attributes) + generate_attribute_list (name_selector_entry); +} + +static gchar * +get_destination_textrep (ENameSelectorEntry *name_selector_entry, + EDestination *destination) +{ + gboolean show_email = e_name_selector_entry_get_show_address (name_selector_entry); + EContact *contact; + + g_return_val_if_fail (destination != NULL, NULL); + + contact = e_destination_get_contact (destination); + + if (!show_email) { + if (contact && !e_contact_get (contact, E_CONTACT_IS_LIST)) { + GList *email_list; + + email_list = e_contact_get (contact, E_CONTACT_EMAIL); + show_email = g_list_length (email_list) > 1; + deep_free_list (email_list); + } + } + + /* do not show emails for contact lists even user forces it */ + if (show_email && contact && e_contact_get (contact, E_CONTACT_IS_LIST)) + show_email = FALSE; + + return sanitize_string (e_destination_get_textrep (destination, show_email)); +} + +static void +sync_destination_at_position (ENameSelectorEntry *name_selector_entry, + gint range_pos, + gint *cursor_pos) +{ + EDestination *destination; + const gchar *text; + gchar *address; + gint address_len; + gint range_start, range_end; + + /* Get the destination we're looking at. Note that the entry may be empty, and so + * there may not be one. */ + destination = find_destination_at_position (name_selector_entry, range_pos); + if (!destination) + return; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + if (!get_range_at_position (text, range_pos, &range_start, &range_end)) { + g_warning ("ENameSelectorEntry is out of sync with model!"); + return; + } + + address = get_destination_textrep (name_selector_entry, destination); + address_len = g_utf8_strlen (address, -1); + + if (cursor_pos) { + /* Update cursor placement */ + if (*cursor_pos >= range_end) + *cursor_pos += address_len - (range_end - range_start); + else if (*cursor_pos > range_start) + *cursor_pos = range_start + address_len; + } + + g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry); + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end); + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), address, -1, &range_start); + + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); + + generate_attribute_list (name_selector_entry); + g_free (address); +} + +static void +remove_destination_by_index (ENameSelectorEntry *name_selector_entry, + gint index) +{ + EDestination *destination; + + destination = find_destination_by_index (name_selector_entry, index); + if (destination) { + g_signal_handlers_block_by_func ( + name_selector_entry->priv->destination_store, + destination_row_deleted, name_selector_entry); + e_destination_store_remove_destination ( + name_selector_entry->priv->destination_store, + destination); + g_signal_handlers_unblock_by_func ( + name_selector_entry->priv->destination_store, + destination_row_deleted, name_selector_entry); + } +} + +static void +post_insert_update (ENameSelectorEntry *name_selector_entry, + gint position) +{ + const gchar *text; + glong length; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + length = g_utf8_strlen (text, -1); + text = g_utf8_next_char (text); + + if (*text == '\0') { + /* First and only character, create initial destination. */ + insert_destination_at_position (name_selector_entry, 0); + } else { + /* Modified an existing destination. */ + modify_destination_at_position (name_selector_entry, position); + } + + /* If editing within the string, regenerate attributes. */ + if (position < length) + generate_attribute_list (name_selector_entry); +} + +/* Returns the number of characters inserted */ +static gint +insert_unichar (ENameSelectorEntry *name_selector_entry, + gint *pos, + gunichar c) +{ + const gchar *text; + gunichar str_context[4]; + gchar buf[7]; + gint len; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + get_utf8_string_context (text, *pos, str_context, 4); + + /* Space is not allowed: + * - Before or after another space. + * - At start of string. */ + + if (c == ' ' && (str_context[1] == ' ' || str_context[1] == '\0' || str_context[2] == ' ')) + return 0; + + /* Comma is not allowed: + * - After another comma. + * - At start of string. */ + + if (c == ',' && !is_quoted_at (text, *pos)) { + gint start_pos; + gint end_pos; + gboolean at_start = FALSE; + gboolean at_end = FALSE; + + if (str_context[1] == ',' || str_context[1] == '\0') + return 0; + + /* We do this so we can avoid disturbing destinations with completed contacts + * either before or after the destination being inserted. */ + get_range_at_position (text, *pos, &start_pos, &end_pos); + if (*pos <= start_pos) + at_start = TRUE; + if (*pos >= end_pos) + at_end = TRUE; + + /* Must insert comma first, so modify_destination_at_position can do its job + * correctly, splitting up the contact if necessary. */ + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, pos); + + /* Update model */ + g_assert (*pos >= 2); + + /* If we inserted the comma at the end of, or in the middle of, an existing + * address, add a new destination for what appears after comma. Else, we + * have to add a destination for what appears before comma (a blank one). */ + if (at_end) { + /* End: Add last, sync first */ + insert_destination_at_position (name_selector_entry, *pos); + sync_destination_at_position (name_selector_entry, *pos - 2, pos); + /* Sync generates the attributes list */ + } else if (at_start) { + /* Start: Add first */ + insert_destination_at_position (name_selector_entry, *pos - 2); + generate_attribute_list (name_selector_entry); + } else { + /* Middle: */ + insert_destination_at_position (name_selector_entry, *pos); + modify_destination_at_position (name_selector_entry, *pos - 2); + generate_attribute_list (name_selector_entry); + } + + return 2; + } + + /* Generic case. Allowed spaces also end up here. */ + + len = g_unichar_to_utf8 (c, buf); + buf[len] = '\0'; + + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), buf, -1, pos); + + post_insert_update (name_selector_entry, *pos); + + return 1; +} + +static void +user_insert_text (ENameSelectorEntry *name_selector_entry, + gchar *new_text, + gint new_text_length, + gint *position, + gpointer user_data) +{ + gint chars_inserted = 0; + gboolean fast_insert; + + g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry); + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + fast_insert = + (g_utf8_strchr (new_text, new_text_length, ' ') == NULL) && + (g_utf8_strchr (new_text, new_text_length, ',') == NULL); + + /* If the text to insert does not contain spaces or commas, + * insert all of it at once. This avoids confusing on-going + * input method behavior. */ + if (fast_insert) { + gint old_position = *position; + + gtk_editable_insert_text ( + GTK_EDITABLE (name_selector_entry), + new_text, new_text_length, position); + + chars_inserted = *position - old_position; + if (chars_inserted > 0) + post_insert_update (name_selector_entry, *position); + + /* Otherwise, apply some rules as to where spaces and commas + * can be inserted, and insert a trailing space after comma. */ + } else { + const gchar *cp; + + for (cp = new_text; *cp; cp = g_utf8_next_char (cp)) { + gunichar uc = g_utf8_get_char (cp); + insert_unichar (name_selector_entry, position, uc); + chars_inserted++; + } + } + + if (chars_inserted >= 1) { + /* If the user inserted one character, kick off completion */ + re_set_timeout (name_selector_entry->priv->update_completions_cb_id, update_completions_on_timeout_cb, name_selector_entry); + re_set_timeout (name_selector_entry->priv->type_ahead_complete_cb_id, type_ahead_complete_on_timeout_cb, name_selector_entry); + } + + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); + + g_signal_stop_emission_by_name (name_selector_entry, "insert_text"); +} + +static void +user_delete_text (ENameSelectorEntry *name_selector_entry, + gint start_pos, + gint end_pos, + gpointer user_data) +{ + const gchar *text; + gint index_start, index_end; + gint selection_start, selection_end; + gunichar str_context[2], str_b_context[2]; + gint len; + gint i; + gboolean del_space = FALSE, del_comma = FALSE; + + if (start_pos == end_pos) + return; + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + len = g_utf8_strlen (text, -1); + + if (end_pos == -1) + end_pos = len; + + gtk_editable_get_selection_bounds ( + GTK_EDITABLE (name_selector_entry), + &selection_start, &selection_end); + + get_utf8_string_context (text, start_pos, str_context, 2); + get_utf8_string_context (text, end_pos, str_b_context, 2); + + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + if (end_pos - start_pos == 1) { + /* Might be backspace; update completion model so dropdown is accurate */ + re_set_timeout (name_selector_entry->priv->update_completions_cb_id, update_completions_on_timeout_cb, name_selector_entry); + } + + index_start = get_index_at_position (text, start_pos); + index_end = get_index_at_position (text, end_pos); + + g_signal_stop_emission_by_name (name_selector_entry, "delete_text"); + + /* If the deletion touches more than one destination, the first one is changed + * and the rest are removed. If the last destination wasn't completely deleted, + * it becomes part of the first one, since the separator between them was + * removed. + * + * Here, we let the model know about removals. */ + for (i = index_end; i > index_start; i--) { + EDestination *destination = find_destination_by_index (name_selector_entry, i); + gint range_start, range_end; + gchar *ttext; + const gchar *email = NULL; + gboolean sel = FALSE; + + if (destination) + email = e_destination_get_textrep (destination, TRUE); + + if (!email || !*email) + continue; + + if (!get_range_by_index (text, i, &range_start, &range_end)) { + g_warning ("ENameSelectorEntry is out of sync with model!"); + return; + } + + if ((selection_start < range_start && selection_end > range_start) || + (selection_end > range_start && selection_end < range_end)) + sel = TRUE; + + if (!sel) { + g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry); + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end); + + ttext = sanitize_string (email); + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ttext, -1, &range_start); + g_free (ttext); + + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); + + } + + remove_destination_by_index (name_selector_entry, i); + } + + /* Do the actual deletion */ + + if (end_pos == start_pos +1 && index_end == index_start) { + /* We could be just deleting the empty text */ + gchar *c; + + /* Get the actual deleted text */ + c = gtk_editable_get_chars (GTK_EDITABLE (name_selector_entry), start_pos, start_pos + 1); + + if ( c[0] == ' ') { + /* If we are at the beginning or removing junk space, let us ignore it */ + del_space = TRUE; + } + g_free (c); + } else if (end_pos == start_pos +1 && index_end == index_start + 1) { + /* We could be just deleting the empty text */ + gchar *c; + + /* Get the actual deleted text */ + c = gtk_editable_get_chars (GTK_EDITABLE (name_selector_entry), start_pos, start_pos + 1); + + if ( c[0] == ',' && !is_quoted_at (text, start_pos)) { + /* If we are at the beginning or removing junk space, let us ignore it */ + del_comma = TRUE; + } + g_free (c); + } + + if (del_comma) { + gint range_start=-1, range_end; + EDestination *dest = find_destination_by_index (name_selector_entry, index_end); + /* If we have deleted the last comma, let us autocomplete normally + */ + + if (dest && len - end_pos != 0) { + + EDestination *destination1 = find_destination_by_index (name_selector_entry, index_start); + gchar *ttext; + const gchar *email = NULL; + + if (destination1) + email = e_destination_get_textrep (destination1, TRUE); + + if (email && *email) { + + if (!get_range_by_index (text, i, &range_start, &range_end)) { + g_warning ("ENameSelectorEntry is out of sync with model!"); + return; + } + + g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry); + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end); + + ttext = sanitize_string (email); + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ttext, -1, &range_start); + g_free (ttext); + + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); + } + + if (range_start != -1) { + start_pos = range_start; + end_pos = start_pos + 1; + gtk_editable_set_position (GTK_EDITABLE (name_selector_entry),start_pos); + } + } + } + gtk_editable_delete_text ( + GTK_EDITABLE (name_selector_entry), + start_pos, end_pos); + + /*If the user is deleting a '"' new destinations have to be created for ',' between the quoted text + Like "fd,ty,uy" is a one entity, but if you remove the quotes it has to be broken doan into 3 seperate + addresses. + */ + + if (str_b_context[1] == '"') { + const gchar *p; + gint j; + p = text + end_pos; + for (p = text + (end_pos - 1), j = end_pos - 1; *p && *p != '"' ; p = g_utf8_next_char (p), j++) { + gunichar c = g_utf8_get_char (p); + if (c == ',') { + insert_destination_at_position (name_selector_entry, j + 1); + } + } + + } + + /* Let model know about changes */ + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + if (!*text || strlen (text) <= 0) { + /* If the entry was completely cleared, remove the initial destination too */ + remove_destination_by_index (name_selector_entry, 0); + generate_attribute_list (name_selector_entry); + } else if (!del_space) { + modify_destination_at_position (name_selector_entry, start_pos); + } + + /* If editing within the string, we need to regenerate attributes */ + if (end_pos < len) + generate_attribute_list (name_selector_entry); + + /* Prevent type-ahead completion */ + if (name_selector_entry->priv->type_ahead_complete_cb_id) { + g_source_remove (name_selector_entry->priv->type_ahead_complete_cb_id); + name_selector_entry->priv->type_ahead_complete_cb_id = 0; + } + + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); +} + +static gboolean +completion_match_selected (ENameSelectorEntry *name_selector_entry, + ETreeModelGenerator *email_generator_model, + GtkTreeIter *generator_iter) +{ + EContact *contact; + EBookClient *book_client; + EDestination *destination; + gint cursor_pos; + GtkTreeIter contact_iter; + gint email_n; + + if (!name_selector_entry->priv->contact_store) + return FALSE; + + g_return_val_if_fail (name_selector_entry->priv->email_generator == email_generator_model, FALSE); + + e_tree_model_generator_convert_iter_to_child_iter ( + email_generator_model, + &contact_iter, &email_n, + generator_iter); + + contact = e_contact_store_get_contact (name_selector_entry->priv->contact_store, &contact_iter); + book_client = e_contact_store_get_client (name_selector_entry->priv->contact_store, &contact_iter); + cursor_pos = gtk_editable_get_position (GTK_EDITABLE (name_selector_entry)); + + /* Set the contact in the model's destination */ + + destination = find_destination_at_position (name_selector_entry, cursor_pos); + e_destination_set_contact (destination, contact, email_n); + if (book_client) + e_destination_set_client (destination, book_client); + sync_destination_at_position (name_selector_entry, cursor_pos, &cursor_pos); + + g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry); + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &cursor_pos); + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); + + /*Add destination at end for next entry*/ + insert_destination_at_position (name_selector_entry, cursor_pos); + /* Place cursor at end of address */ + + gtk_editable_set_position (GTK_EDITABLE (name_selector_entry), cursor_pos); + g_signal_emit (name_selector_entry, signals[UPDATED], 0, destination, NULL); + return TRUE; +} + +static void +entry_activate (ENameSelectorEntry *name_selector_entry) +{ + gint cursor_pos; + gint range_start, range_end; + ENameSelectorEntryPrivate *priv; + EDestination *destination; + gint range_len; + const gchar *text; + gchar *cue_str; + + cursor_pos = gtk_editable_get_position (GTK_EDITABLE (name_selector_entry)); + if (cursor_pos < 0) + return; + + priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry); + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + if (!get_range_at_position (text, cursor_pos, &range_start, &range_end)) + return; + + range_len = range_end - range_start; + if (range_len < priv->minimum_query_length) + return; + + destination = find_destination_at_position (name_selector_entry, cursor_pos); + if (!destination) + return; + + cue_str = get_entry_substring (name_selector_entry, range_start, range_end); +#if 0 + if (!find_existing_completion (name_selector_entry, cue_str, &contact, + &textrep, &matched_field)) { + g_free (cue_str); + return; + } +#endif + g_free (cue_str); + sync_destination_at_position (name_selector_entry, cursor_pos, &cursor_pos); + + /* Place cursor at end of address */ + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + get_range_at_position (text, cursor_pos, &range_start, &range_end); + + if (priv->is_completing) { + gchar *str_context = NULL; + + str_context = gtk_editable_get_chars (GTK_EDITABLE (name_selector_entry), range_end, range_end + 1); + + if (str_context[0] != ',') { + /* At the end*/ + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &range_end); + } else { + /* In the middle */ + gint newpos = strlen (text); + + /* Doing this we can make sure that It wont ask for completion again. */ + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &newpos); + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), newpos - 2, newpos); + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + /* Move it close to next destination*/ + range_end = range_end + 2; + + } + g_free (str_context); + } + + gtk_editable_set_position (GTK_EDITABLE (name_selector_entry), range_end); + g_signal_emit (name_selector_entry, signals[UPDATED], 0, destination, NULL); + + if (priv->is_completing) + clear_completion_model (name_selector_entry); +} + +static void +update_text (ENameSelectorEntry *name_selector_entry, + const gchar *text) +{ + gint start = 0, end = 0; + gboolean has_selection; + + has_selection = gtk_editable_get_selection_bounds (GTK_EDITABLE (name_selector_entry), &start, &end); + + gtk_entry_set_text (GTK_ENTRY (name_selector_entry), text); + + if (has_selection) + gtk_editable_select_region (GTK_EDITABLE (name_selector_entry), start, end); +} + +static void +sanitize_entry (ENameSelectorEntry *name_selector_entry) +{ + gint n; + GList *l, *known, *del = NULL; + GString *str = g_string_new (""); + + g_signal_handlers_block_matched (name_selector_entry, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry); + g_signal_handlers_block_matched (name_selector_entry->priv->destination_store, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry); + + known = e_destination_store_list_destinations (name_selector_entry->priv->destination_store); + for (l = known, n = 0; l != NULL; l = l->next, n++) { + EDestination *dest = l->data; + + if (!dest || !e_destination_get_address (dest)) + del = g_list_prepend (del, GINT_TO_POINTER (n)); + else { + gchar *text; + + text = get_destination_textrep (name_selector_entry, dest); + if (text) { + if (str->str && str->str[0]) + g_string_append (str, ", "); + + g_string_append (str, text); + } + g_free (text); + } + } + g_list_free (known); + + for (l = del; l != NULL; l = l->next) { + e_destination_store_remove_destination_nth (name_selector_entry->priv->destination_store, GPOINTER_TO_INT (l->data)); + } + g_list_free (del); + + update_text (name_selector_entry, str->str); + + g_string_free (str, TRUE); + + g_signal_handlers_unblock_matched (name_selector_entry->priv->destination_store, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry); + g_signal_handlers_unblock_matched (name_selector_entry, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry); + + generate_attribute_list (name_selector_entry); +} + +static gboolean +user_focus_in (ENameSelectorEntry *name_selector_entry, + GdkEventFocus *event_focus) +{ + gint n; + GList *l, *known; + GString *str = g_string_new (""); + EDestination *dest_dummy = e_destination_new (); + + g_signal_handlers_block_matched (name_selector_entry, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry); + g_signal_handlers_block_matched (name_selector_entry->priv->destination_store, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry); + + known = e_destination_store_list_destinations (name_selector_entry->priv->destination_store); + for (l = known, n = 0; l != NULL; l = l->next, n++) { + EDestination *dest = l->data; + + if (dest) { + gchar *text; + + text = get_destination_textrep (name_selector_entry, dest); + if (text) { + if (str->str && str->str[0]) + g_string_append (str, ", "); + + g_string_append (str, text); + } + g_free (text); + } + } + g_list_free (known); + + /* Add a blank destination */ + e_destination_store_append_destination (name_selector_entry->priv->destination_store, dest_dummy); + if (str->str && str->str[0]) + g_string_append (str, ", "); + + gtk_entry_set_text (GTK_ENTRY (name_selector_entry), str->str); + + g_string_free (str, TRUE); + + g_signal_handlers_unblock_matched (name_selector_entry->priv->destination_store, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry); + g_signal_handlers_unblock_matched (name_selector_entry, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry); + + generate_attribute_list (name_selector_entry); + + return FALSE; +} + +static gboolean +user_focus_out (ENameSelectorEntry *name_selector_entry, + GdkEventFocus *event_focus) +{ + if (!event_focus->in) { + entry_activate (name_selector_entry); + } + + if (name_selector_entry->priv->type_ahead_complete_cb_id) { + g_source_remove (name_selector_entry->priv->type_ahead_complete_cb_id); + name_selector_entry->priv->type_ahead_complete_cb_id = 0; + } + + if (name_selector_entry->priv->update_completions_cb_id) { + g_source_remove (name_selector_entry->priv->update_completions_cb_id); + name_selector_entry->priv->update_completions_cb_id = 0; + } + + clear_completion_model (name_selector_entry); + + if (!event_focus->in) { + sanitize_entry (name_selector_entry); + } + + return FALSE; +} + +static void +deep_free_list (GList *list) +{ + GList *l; + + for (l = list; l; l = g_list_next (l)) + g_free (l->data); + + g_list_free (list); +} + +/* Given a widget, determines the height that text will normally be drawn. */ +static guint +entry_height (GtkWidget *widget) +{ + PangoLayout *layout; + gint bound; + + g_return_val_if_fail (widget != NULL, 0); + + layout = gtk_widget_create_pango_layout (widget, NULL); + + pango_layout_get_pixel_size (layout, NULL, &bound); + + return bound; +} + +static void +contact_layout_pixbuffer (GtkCellLayout *cell_layout, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + ENameSelectorEntry *name_selector_entry) +{ + EContact *contact; + GtkTreeIter generator_iter; + GtkTreeIter contact_store_iter; + gint email_n; + EContactPhoto *photo; + GdkPixbuf *pixbuf = NULL; + + if (!name_selector_entry->priv->contact_store) + return; + + gtk_tree_model_filter_convert_iter_to_child_iter ( + GTK_TREE_MODEL_FILTER (model), + &generator_iter, iter); + e_tree_model_generator_convert_iter_to_child_iter ( + name_selector_entry->priv->email_generator, + &contact_store_iter, &email_n, + &generator_iter); + + contact = e_contact_store_get_contact (name_selector_entry->priv->contact_store, &contact_store_iter); + if (!contact) { + g_object_set (cell, "pixbuf", pixbuf, NULL); + return; + } + + photo = e_contact_get (contact, E_CONTACT_PHOTO); + if (photo && photo->type == E_CONTACT_PHOTO_TYPE_INLINED) { + guint max_height = entry_height (GTK_WIDGET (name_selector_entry)); + GdkPixbufLoader *loader; + + loader = gdk_pixbuf_loader_new (); + if (gdk_pixbuf_loader_write (loader, (guchar *) photo->data.inlined.data, photo->data.inlined.length, NULL) && + gdk_pixbuf_loader_close (loader, NULL)) { + pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); + if (pixbuf) + g_object_ref (pixbuf); + } + g_object_unref (loader); + + if (pixbuf) { + gint w, h; + gdouble scale = 1.0; + + w = gdk_pixbuf_get_width (pixbuf); + h = gdk_pixbuf_get_height (pixbuf); + + if (h > w) + scale = max_height / (double) h; + else + scale = max_height / (double) w; + + if (scale < 1.0) { + GdkPixbuf *tmp; + + tmp = gdk_pixbuf_scale_simple (pixbuf, w * scale, h * scale, GDK_INTERP_BILINEAR); + g_object_unref (pixbuf); + pixbuf = tmp; + } + + } + } + + e_contact_photo_free (photo); + + g_object_set (cell, "pixbuf", pixbuf, NULL); + + if (pixbuf) + g_object_unref (pixbuf); +} + +static void +contact_layout_formatter (GtkCellLayout *cell_layout, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + ENameSelectorEntry *name_selector_entry) +{ + EContact *contact; + GtkTreeIter generator_iter; + GtkTreeIter contact_store_iter; + GList *email_list; + gchar *string; + gchar *file_as_str; + gchar *email_str; + gint email_n; + + if (!name_selector_entry->priv->contact_store) + return; + + gtk_tree_model_filter_convert_iter_to_child_iter ( + GTK_TREE_MODEL_FILTER (model), + &generator_iter, iter); + e_tree_model_generator_convert_iter_to_child_iter ( + name_selector_entry->priv->email_generator, + &contact_store_iter, &email_n, + &generator_iter); + + contact = e_contact_store_get_contact (name_selector_entry->priv->contact_store, &contact_store_iter); + email_list = e_contact_get (contact, E_CONTACT_EMAIL); + email_str = g_list_nth_data (email_list, email_n); + file_as_str = e_contact_get (contact, E_CONTACT_FILE_AS); + + if (e_contact_get (contact, E_CONTACT_IS_LIST)) { + string = g_strdup_printf ("%s", file_as_str ? file_as_str : "?"); + } else { + string = g_strdup_printf ( + "%s%s<%s>", file_as_str ? file_as_str : "", + file_as_str ? " " : "", + email_str ? email_str : ""); + } + + g_free (file_as_str); + deep_free_list (email_list); + + g_object_set (cell, "text", string, NULL); + g_free (string); +} + +static gint +generate_contact_rows (EContactStore *contact_store, + GtkTreeIter *iter, + ENameSelectorEntry *name_selector_entry) +{ + EContact *contact; + const gchar *contact_uid; + GList *email_list; + gint n_rows; + + contact = e_contact_store_get_contact (contact_store, iter); + g_assert (contact != NULL); + + contact_uid = e_contact_get_const (contact, E_CONTACT_UID); + if (!contact_uid) + return 0; /* Can happen with broken databases */ + + if (e_contact_get (contact, E_CONTACT_IS_LIST)) + return 1; + + email_list = e_contact_get (contact, E_CONTACT_EMAIL); + n_rows = g_list_length (email_list); + deep_free_list (email_list); + + return n_rows; +} + +static void +ensure_type_ahead_complete_on_timeout (ENameSelectorEntry *name_selector_entry) +{ + re_set_timeout ( + name_selector_entry->priv->type_ahead_complete_cb_id, + type_ahead_complete_on_timeout_cb, name_selector_entry); +} + +static void +setup_contact_store (ENameSelectorEntry *name_selector_entry) +{ + if (name_selector_entry->priv->email_generator) { + g_object_unref (name_selector_entry->priv->email_generator); + name_selector_entry->priv->email_generator = NULL; + } + + if (name_selector_entry->priv->contact_store) { + name_selector_entry->priv->email_generator = + e_tree_model_generator_new ( + GTK_TREE_MODEL ( + name_selector_entry->priv->contact_store)); + + e_tree_model_generator_set_generate_func ( + name_selector_entry->priv->email_generator, + (ETreeModelGeneratorGenerateFunc) generate_contact_rows, + name_selector_entry, NULL); + + /* Assign the store to the entry completion */ + + gtk_entry_completion_set_model ( + name_selector_entry->priv->entry_completion, + GTK_TREE_MODEL ( + name_selector_entry->priv->email_generator)); + + /* Set up callback for incoming matches */ + g_signal_connect_swapped ( + name_selector_entry->priv->contact_store, "row-inserted", + G_CALLBACK (ensure_type_ahead_complete_on_timeout), name_selector_entry); + } else { + /* Remove the store from the entry completion */ + + gtk_entry_completion_set_model (name_selector_entry->priv->entry_completion, NULL); + } +} + +static void +book_loaded_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EContactStore *contact_store = user_data; + ESource *source = E_SOURCE (source_object); + EBookClient *book_client; + EClient *client = NULL; + GError *error = NULL; + + e_client_utils_open_new_finish (source, result, &client, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warn_if_fail (client == NULL); + g_error_free (error); + goto exit; + } + + if (error != NULL) { + g_warning ("%s", error->message); + g_warn_if_fail (client == NULL); + g_error_free (error); + goto exit; + } + + book_client = E_BOOK_CLIENT (client); + + g_return_if_fail (E_IS_BOOK_CLIENT (book_client)); + e_contact_store_add_client (contact_store, book_client); + g_object_unref (book_client); + + exit: + g_object_unref (contact_store); +} + +static void +setup_default_contact_store (ENameSelectorEntry *name_selector_entry) +{ + ESourceRegistry *registry; + EContactStore *contact_store; + GList *list, *iter; + const gchar *extension_name; + + g_return_if_fail (name_selector_entry->priv->contact_store == NULL); + + /* Create a book for each completion source, and assign them to the contact store */ + + contact_store = e_contact_store_new (); + name_selector_entry->priv->contact_store = contact_store; + + extension_name = E_SOURCE_EXTENSION_ADDRESS_BOOK; + registry = e_name_selector_entry_get_registry (name_selector_entry); + + /* An ESourceRegistry should have been set by now. */ + g_return_if_fail (registry != NULL); + + list = e_source_registry_list_sources (registry, extension_name); + + for (iter = list; iter != NULL; iter = g_list_next (iter)) { + ESource *source = E_SOURCE (iter->data); + ESourceAutocomplete *extension; + GCancellable *cancellable; + const gchar *extension_name; + + extension_name = E_SOURCE_EXTENSION_AUTOCOMPLETE; + extension = e_source_get_extension (source, extension_name); + + /* Skip disabled address books. */ + if (!e_source_registry_check_enabled (registry, source)) + continue; + + /* Skip non-completion address books. */ + if (!e_source_autocomplete_get_include_me (extension)) + continue; + + cancellable = g_cancellable_new (); + + g_queue_push_tail ( + &name_selector_entry->priv->cancellables, + cancellable); + + e_client_utils_open_new ( + source, E_CLIENT_SOURCE_TYPE_CONTACTS, TRUE, cancellable, + book_loaded_cb, g_object_ref (contact_store)); + } + + g_list_free_full (list, (GDestroyNotify) g_object_unref); + + setup_contact_store (name_selector_entry); +} + +static void +destination_row_changed (ENameSelectorEntry *name_selector_entry, + GtkTreePath *path, + GtkTreeIter *iter) +{ + EDestination *destination; + const gchar *entry_text; + gchar *text; + gint range_start, range_end; + gint n; + + n = gtk_tree_path_get_indices (path)[0]; + destination = e_destination_store_get_destination (name_selector_entry->priv->destination_store, iter); + + if (!destination) + return; + + g_assert (n >= 0); + + entry_text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + if (!get_range_by_index (entry_text, n, &range_start, &range_end)) { + g_warning ("ENameSelectorEntry is out of sync with model!"); + return; + } + + g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry); + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end); + + text = get_destination_textrep (name_selector_entry, destination); + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), text, -1, &range_start); + g_free (text); + + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); + + clear_completion_model (name_selector_entry); + generate_attribute_list (name_selector_entry); +} + +static void +destination_row_inserted (ENameSelectorEntry *name_selector_entry, + GtkTreePath *path, + GtkTreeIter *iter) +{ + EDestination *destination; + const gchar *entry_text; + gchar *text; + gboolean comma_before = FALSE; + gboolean comma_after = FALSE; + gint range_start, range_end; + gint insert_pos; + gint n; + + n = gtk_tree_path_get_indices (path)[0]; + destination = e_destination_store_get_destination (name_selector_entry->priv->destination_store, iter); + + g_assert (n >= 0); + g_assert (destination != NULL); + + entry_text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + + if (get_range_by_index (entry_text, n, &range_start, &range_end) && range_start != range_end) { + /* Another destination comes after us */ + insert_pos = range_start; + comma_after = TRUE; + } else if (n > 0 && get_range_by_index (entry_text, n - 1, &range_start, &range_end)) { + /* Another destination comes before us */ + insert_pos = range_end; + comma_before = TRUE; + } else if (n == 0) { + /* We're the sole destination */ + insert_pos = 0; + } else { + g_warning ("ENameSelectorEntry is out of sync with model!"); + return; + } + + g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry); + + if (comma_before) + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &insert_pos); + + text = get_destination_textrep (name_selector_entry, destination); + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), text, -1, &insert_pos); + g_free (text); + + if (comma_after) + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &insert_pos); + + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); + + clear_completion_model (name_selector_entry); + generate_attribute_list (name_selector_entry); +} + +static void +destination_row_deleted (ENameSelectorEntry *name_selector_entry, + GtkTreePath *path) +{ + const gchar *text; + gboolean deleted_comma = FALSE; + gint range_start, range_end; + gchar *p0; + gint n; + + n = gtk_tree_path_get_indices (path)[0]; + g_assert (n >= 0); + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + + if (!get_range_by_index (text, n, &range_start, &range_end)) { + g_warning ("ENameSelectorEntry is out of sync with model!"); + return; + } + + /* Expand range for deletion forwards */ + for (p0 = g_utf8_offset_to_pointer (text, range_end); *p0; + p0 = g_utf8_next_char (p0), range_end++) { + gunichar c = g_utf8_get_char (p0); + + /* Gobble spaces directly after comma */ + if (c != ' ' && deleted_comma) { + range_end--; + break; + } + + if (c == ',') { + deleted_comma = TRUE; + range_end++; + } + } + + /* Expand range for deletion backwards */ + for (p0 = g_utf8_offset_to_pointer (text, range_start); range_start > 0; + p0 = g_utf8_prev_char (p0), range_start--) { + gunichar c = g_utf8_get_char (p0); + + if (c == ',') { + if (!deleted_comma) { + deleted_comma = TRUE; + break; + } + + range_start++; + + /* Leave a space in front; we deleted the comma and spaces before the + * following destination */ + p0 = g_utf8_next_char (p0); + c = g_utf8_get_char (p0); + if (c == ' ') + range_start++; + + break; + } + } + + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end); + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + clear_completion_model (name_selector_entry); + generate_attribute_list (name_selector_entry); +} + +static void +setup_destination_store (ENameSelectorEntry *name_selector_entry) +{ + GtkTreeIter iter; + + g_signal_connect_swapped ( + name_selector_entry->priv->destination_store, "row-changed", + G_CALLBACK (destination_row_changed), name_selector_entry); + g_signal_connect_swapped ( + name_selector_entry->priv->destination_store, "row-deleted", + G_CALLBACK (destination_row_deleted), name_selector_entry); + g_signal_connect_swapped ( + name_selector_entry->priv->destination_store, "row-inserted", + G_CALLBACK (destination_row_inserted), name_selector_entry); + + if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (name_selector_entry->priv->destination_store), &iter)) + return; + + do { + GtkTreePath *path; + + path = gtk_tree_model_get_path (GTK_TREE_MODEL (name_selector_entry->priv->destination_store), &iter); + g_assert (path); + + destination_row_inserted (name_selector_entry, path, &iter); + } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (name_selector_entry->priv->destination_store), &iter)); +} + +static gboolean +prepare_popup_destination (ENameSelectorEntry *name_selector_entry, + GdkEventButton *event_button) +{ + EDestination *destination; + PangoLayout *layout; + gint layout_offset_x; + gint layout_offset_y; + gint x, y; + gint index; + + if (event_button->type != GDK_BUTTON_PRESS) + return FALSE; + + if (event_button->button != 3) + return FALSE; + + if (name_selector_entry->priv->popup_destination) { + g_object_unref (name_selector_entry->priv->popup_destination); + name_selector_entry->priv->popup_destination = NULL; + } + + gtk_entry_get_layout_offsets ( + GTK_ENTRY (name_selector_entry), + &layout_offset_x, &layout_offset_y); + x = (event_button->x + 0.5) - layout_offset_x; + y = (event_button->y + 0.5) - layout_offset_y; + + if (x < 0 || y < 0) + return FALSE; + + layout = gtk_entry_get_layout (GTK_ENTRY (name_selector_entry)); + if (!pango_layout_xy_to_index (layout, x * PANGO_SCALE, y * PANGO_SCALE, &index, NULL)) + return FALSE; + + index = gtk_entry_layout_index_to_text_index (GTK_ENTRY (name_selector_entry), index); + destination = find_destination_at_position (name_selector_entry, index); + /* FIXME: Add this to a private variable, in ENameSelectorEntry Class*/ + g_object_set_data ((GObject *) name_selector_entry, "index", GINT_TO_POINTER (index)); + + if (!destination || !e_destination_get_contact (destination)) + return FALSE; + + /* TODO: Unref destination when we finalize */ + name_selector_entry->priv->popup_destination = g_object_ref (destination); + return FALSE; +} + +static EBookClient * +find_client_by_contact (GSList *clients, + const gchar *contact_uid, + const gchar *source_uid) +{ + GSList *l; + + if (source_uid && *source_uid) { + /* this is much quicket than asking each client for an existence */ + for (l = clients; l; l = g_slist_next (l)) { + EBookClient *client = l->data; + ESource *source = e_client_get_source (E_CLIENT (client)); + + if (!source) + continue; + + if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0) + return client; + } + } + + for (l = clients; l; l = g_slist_next (l)) { + EBookClient *client = l->data; + EContact *contact = NULL; + gboolean result; + + result = e_book_client_get_contact_sync (client, contact_uid, &contact, NULL, NULL); + if (contact) + g_object_unref (contact); + + if (result) + return client; + } + + return NULL; +} + +static void +editor_closed_cb (GtkWidget *editor, + gpointer data) +{ + EContact *contact; + gchar *contact_uid; + EDestination *destination; + GSList *clients; + EBookClient *book_client; + gint email_num; + ENameSelectorEntry *name_selector_entry = E_NAME_SELECTOR_ENTRY (data); + + destination = name_selector_entry->priv->popup_destination; + contact = e_destination_get_contact (destination); + if (!contact) { + g_object_unref (name_selector_entry); + return; + } + + contact_uid = e_contact_get (contact, E_CONTACT_UID); + if (!contact_uid) { + g_object_unref (contact); + g_object_unref (name_selector_entry); + return; + } + + if (name_selector_entry->priv->contact_store) { + clients = e_contact_store_get_clients (name_selector_entry->priv->contact_store); + book_client = find_client_by_contact (clients, contact_uid, e_destination_get_source_uid (destination)); + g_slist_free (clients); + } else { + book_client = NULL; + } + + if (book_client) { + contact = NULL; + + g_warn_if_fail (e_book_client_get_contact_sync (book_client, contact_uid, &contact, NULL, NULL)); + email_num = e_destination_get_email_num (destination); + e_destination_set_contact (destination, contact, email_num); + e_destination_set_client (destination, book_client); + } else { + contact = NULL; + } + + g_free (contact_uid); + if (contact) + g_object_unref (contact); + g_object_unref (name_selector_entry); +} + +/* To parse something like... + * =?UTF-8?Q?=E0=A4=95=E0=A4=95=E0=A4=AC=E0=A5=82=E0=A5=8B=E0=A5=87?=\t\n=?UTF-8?Q?=E0=A4=B0?=\t\n<aa@aa.ccom> + * and return the decoded representation of name & email parts. + * */ +static gboolean +eab_parse_qp_email (const gchar *string, + gchar **name, + gchar **email) +{ + struct _camel_header_address *address; + gboolean res = FALSE; + + address = camel_header_address_decode (string, "UTF-8"); + + if (!address) + return FALSE; + + /* report success only when we have filled both name and email address */ + if (address->type == CAMEL_HEADER_ADDRESS_NAME && address->name && *address->name && address->v.addr && *address->v.addr) { + *name = g_strdup (address->name); + *email = g_strdup (address->v.addr); + res = TRUE; + } + + camel_header_address_unref (address); + + return res; +} + +static void +popup_activate_inline_expand (ENameSelectorEntry *name_selector_entry, + GtkWidget *menu_item) +{ + const gchar *text; + GString *sanitized_text = g_string_new (""); + EDestination *destination = name_selector_entry->priv->popup_destination; + gint position, start, end; + const GList *dests; + + position = GPOINTER_TO_INT (g_object_get_data ((GObject *) name_selector_entry, "index")); + + for (dests = e_destination_list_get_dests (destination); dests; dests = dests->next) { + const EDestination *dest = dests->data; + gchar *sanitized; + gchar *name = NULL, *email = NULL, *tofree = NULL; + + if (!dest) + continue; + + text = e_destination_get_textrep (dest, TRUE); + + if (!text || !*text) + continue; + + if (eab_parse_qp_email (text, &name, &email)) { + tofree = g_strdup_printf ("%s <%s>", name, email); + text = tofree; + g_free (name); + g_free (email); + } + + sanitized = sanitize_string (text); + g_free (tofree); + if (!sanitized) + continue; + + if (*sanitized) { + if (*sanitized_text->str) + g_string_append (sanitized_text, ", "); + + g_string_append (sanitized_text, sanitized); + } + + g_free (sanitized); + } + + text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); + get_range_at_position (text, position, &start, &end); + gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), start, end); + gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), sanitized_text->str, -1, &start); + g_string_free (sanitized_text, TRUE); + + clear_completion_model (name_selector_entry); + generate_attribute_list (name_selector_entry); +} + +static void +popup_activate_contact (ENameSelectorEntry *name_selector_entry, + GtkWidget *menu_item) +{ + EBookClient *book_client; + GSList *clients; + EDestination *destination; + EContact *contact; + gchar *contact_uid; + + destination = name_selector_entry->priv->popup_destination; + if (!destination) + return; + + contact = e_destination_get_contact (destination); + if (!contact) + return; + + contact_uid = e_contact_get (contact, E_CONTACT_UID); + if (!contact_uid) + return; + + if (name_selector_entry->priv->contact_store) { + clients = e_contact_store_get_clients (name_selector_entry->priv->contact_store); + book_client = find_client_by_contact (clients, contact_uid, e_destination_get_source_uid (destination)); + g_slist_free (clients); + g_free (contact_uid); + } else { + book_client = NULL; + } + + if (!book_client) + return; + + if (e_destination_is_evolution_list (destination)) { + GtkWidget *contact_list_editor; + + if (!name_selector_entry->priv->contact_list_editor_func) + return; + + contact_list_editor = (*name_selector_entry->priv->contact_list_editor_func) (book_client, contact, FALSE, TRUE); + g_object_ref (name_selector_entry); + g_signal_connect ( + contact_list_editor, "editor_closed", + G_CALLBACK (editor_closed_cb), name_selector_entry); + } else { + GtkWidget *contact_editor; + + if (!name_selector_entry->priv->contact_editor_func) + return; + + contact_editor = (*name_selector_entry->priv->contact_editor_func) (book_client, contact, FALSE, TRUE); + g_object_ref (name_selector_entry); + g_signal_connect ( + contact_editor, "editor_closed", + G_CALLBACK (editor_closed_cb), name_selector_entry); + } +} + +static void +popup_activate_email (ENameSelectorEntry *name_selector_entry, + GtkWidget *menu_item) +{ + EDestination *destination; + EContact *contact; + gint email_num; + + destination = name_selector_entry->priv->popup_destination; + if (!destination) + return; + + contact = e_destination_get_contact (destination); + if (!contact) + return; + + email_num = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (menu_item), "order")); + e_destination_set_contact (destination, contact, email_num); +} + +static void +popup_activate_list (EDestination *destination, + GtkWidget *item) +{ + gboolean status = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)); + + e_destination_set_ignored (destination, !status); +} + +static void +popup_activate_cut (ENameSelectorEntry *name_selector_entry, + GtkWidget *menu_item) +{ + EDestination *destination; + const gchar *contact_email; + gchar *pemail = NULL; + GtkClipboard *clipboard; + + destination = name_selector_entry->priv->popup_destination; + contact_email =e_destination_get_textrep (destination, TRUE); + + g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry); + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); + pemail = g_strconcat (contact_email, ",", NULL); + gtk_clipboard_set_text (clipboard, pemail, strlen (pemail)); + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_text (clipboard, pemail, strlen (pemail)); + + gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), 0, 0); + e_destination_store_remove_destination (name_selector_entry->priv->destination_store, destination); + + g_free (pemail); + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); +} + +static void +popup_activate_copy (ENameSelectorEntry *name_selector_entry, + GtkWidget *menu_item) +{ + EDestination *destination; + const gchar *contact_email; + gchar *pemail; + GtkClipboard *clipboard; + + destination = name_selector_entry->priv->popup_destination; + contact_email = e_destination_get_textrep (destination, TRUE); + + g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry); + g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); + + clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); + pemail = g_strconcat (contact_email, ",", NULL); + gtk_clipboard_set_text (clipboard, pemail, strlen (pemail)); + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_text (clipboard, pemail, strlen (pemail)); + g_free (pemail); + g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); + g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry); +} + +static void +destination_set_list (GtkWidget *item, + EDestination *destination) +{ + EContact *contact; + gboolean status = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)); + + contact = e_destination_get_contact (destination); + if (!contact) + return; + + e_destination_set_ignored (destination, !status); +} + +static void +destination_set_email (GtkWidget *item, + EDestination *destination) +{ + gint email_num; + EContact *contact; + + if (!gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item))) + return; + contact = e_destination_get_contact (destination); + if (!contact) + return; + + email_num = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "order")); + e_destination_set_contact (destination, contact, email_num); +} + +static void +populate_popup (ENameSelectorEntry *name_selector_entry, + GtkMenu *menu) +{ + EDestination *destination; + EContact *contact; + GtkWidget *menu_item; + GList *email_list = NULL; + GList *l; + gint i; + gchar *edit_label; + gchar *cut_label; + gchar *copy_label; + gint email_num, len; + GSList *group = NULL; + gboolean is_list; + gboolean show_menu = FALSE; + + destination = name_selector_entry->priv->popup_destination; + if (!destination) + return; + + contact = e_destination_get_contact (destination); + if (!contact) + return; + + /* Prepend the menu items, backwards */ + + /* Separator */ + + menu_item = gtk_separator_menu_item_new (); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + email_num = e_destination_get_email_num (destination); + + /* Addresses */ + is_list = e_contact_get (contact, E_CONTACT_IS_LIST) ? TRUE : FALSE; + if (is_list) { + const GList *dests = e_destination_list_get_dests (destination); + GList *iter; + gint length = g_list_length ((GList *) dests); + + for (iter = (GList *) dests; iter; iter = iter->next) { + EDestination *dest = (EDestination *) iter->data; + const gchar *email = e_destination_get_email (dest); + + if (!email || *email == '\0') + continue; + + if (length > 1) { + menu_item = gtk_check_menu_item_new_with_label (email); + g_signal_connect ( + menu_item, "toggled", + G_CALLBACK (destination_set_list), dest); + } else { + menu_item = gtk_menu_item_new_with_label (email); + } + + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + show_menu = TRUE; + + if (length > 1) { + gtk_check_menu_item_set_active ( + GTK_CHECK_MENU_ITEM (menu_item), + !e_destination_is_ignored (dest)); + g_signal_connect_swapped ( + menu_item, "activate", + G_CALLBACK (popup_activate_list), dest); + } + } + + } else { + email_list = e_contact_get (contact, E_CONTACT_EMAIL); + len = g_list_length (email_list); + + for (l = email_list, i = 0; l; l = g_list_next (l), i++) { + gchar *email = l->data; + + if (!email || *email == '\0') + continue; + + if (len > 1) { + menu_item = gtk_radio_menu_item_new_with_label (group, email); + group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (menu_item)); + g_signal_connect (menu_item, "toggled", G_CALLBACK (destination_set_email), destination); + } else { + menu_item = gtk_menu_item_new_with_label (email); + } + + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + show_menu = TRUE; + g_object_set_data (G_OBJECT (menu_item), "order", GINT_TO_POINTER (i)); + + if (i == email_num && len > 1) { + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menu_item), TRUE); + g_signal_connect_swapped ( + menu_item, "activate", + G_CALLBACK (popup_activate_email), + name_selector_entry); + } + } + } + + /* Separator */ + + if (show_menu) { + menu_item = gtk_separator_menu_item_new (); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + } + + /* Expand a list inline */ + if (is_list) { + /* To Translators: This would be similiar to "Expand MyList Inline" where MyList is a Contact List*/ + edit_label = g_strdup_printf (_("E_xpand %s Inline"), (gchar *) e_contact_get_const (contact, E_CONTACT_FILE_AS)); + menu_item = gtk_menu_item_new_with_mnemonic (edit_label); + g_free (edit_label); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + g_signal_connect_swapped ( + menu_item, "activate", G_CALLBACK (popup_activate_inline_expand), + name_selector_entry); + + /* Separator */ + menu_item = gtk_separator_menu_item_new (); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + } + + /* Copy Contact Item */ + copy_label = g_strdup_printf (_("Cop_y %s"), (gchar *) e_contact_get_const (contact, E_CONTACT_FILE_AS)); + menu_item = gtk_menu_item_new_with_mnemonic (copy_label); + g_free (copy_label); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + + g_signal_connect_swapped ( + menu_item, "activate", G_CALLBACK (popup_activate_copy), + name_selector_entry); + + /* Cut Contact Item */ + cut_label = g_strdup_printf (_("C_ut %s"), (gchar *) e_contact_get_const (contact, E_CONTACT_FILE_AS)); + menu_item = gtk_menu_item_new_with_mnemonic (cut_label); + g_free (cut_label); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + + g_signal_connect_swapped ( + menu_item, "activate", G_CALLBACK (popup_activate_cut), + name_selector_entry); + + if (show_menu) { + menu_item = gtk_separator_menu_item_new (); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + } + + /* Edit Contact item */ + + edit_label = g_strdup_printf (_("_Edit %s"), (gchar *) e_contact_get_const (contact, E_CONTACT_FILE_AS)); + menu_item = gtk_menu_item_new_with_mnemonic (edit_label); + g_free (edit_label); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + + g_signal_connect_swapped ( + menu_item, "activate", G_CALLBACK (popup_activate_contact), + name_selector_entry); + + deep_free_list (email_list); +} + +static void +copy_or_cut_clipboard (ENameSelectorEntry *name_selector_entry, + gboolean is_cut) +{ + GtkClipboard *clipboard; + GtkEditable *editable; + const gchar *text, *cp; + GHashTable *hash; + GHashTableIter iter; + gpointer key, value; + GString *addresses; + gint ii, start, end; + gunichar uc; + + editable = GTK_EDITABLE (name_selector_entry); + text = gtk_entry_get_text (GTK_ENTRY (editable)); + + if (!gtk_editable_get_selection_bounds (editable, &start, &end)) + return; + + g_return_if_fail (end > start); + + hash = g_hash_table_new (g_direct_hash, g_direct_equal); + + ii = end; + cp = g_utf8_offset_to_pointer (text, end); + uc = g_utf8_get_char (cp); + + /* Exclude trailing whitespace and commas. */ + while (ii >= start && (uc == ',' || g_unichar_isspace (uc))) { + cp = g_utf8_prev_char (cp); + uc = g_utf8_get_char (cp); + ii--; + } + + /* Determine the index of each remaining character. */ + while (ii >= start) { + gint index = get_index_at_position (text, ii--); + g_hash_table_insert (hash, GINT_TO_POINTER (index), NULL); + } + + addresses = g_string_new (""); + + g_hash_table_iter_init (&iter, hash); + while (g_hash_table_iter_next (&iter, &key, &value)) { + gint index = GPOINTER_TO_INT (key); + EDestination *dest; + gint rstart, rend; + + if (!get_range_by_index (text, index, &rstart, &rend)) + continue; + + if (rstart < start) { + if (addresses->str && *addresses->str) + g_string_append (addresses, ", "); + + g_string_append_len (addresses, text + start, rend - start); + } else if (rend > end) { + if (addresses->str && *addresses->str) + g_string_append (addresses, ", "); + + g_string_append_len (addresses, text + rstart, end - rstart); + } else { + /* the contact is whole selected */ + dest = find_destination_by_index (name_selector_entry, index); + if (dest && e_destination_get_textrep (dest, TRUE)) { + if (addresses->str && *addresses->str) + g_string_append (addresses, ", "); + + g_string_append (addresses, e_destination_get_textrep (dest, TRUE)); + + /* store the 'dest' as a value for the index */ + g_hash_table_insert (hash, GINT_TO_POINTER (index), dest); + } else + g_string_append_len (addresses, text + rstart, rend - rstart); + } + } + + if (is_cut) + gtk_editable_delete_text (editable, start, end); + + g_hash_table_unref (hash); + + clipboard = gtk_widget_get_clipboard ( + GTK_WIDGET (name_selector_entry), GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_text (clipboard, addresses->str, -1); + + g_string_free (addresses, TRUE); +} + +static void +copy_clipboard (GtkEntry *entry, + ENameSelectorEntry *name_selector_entry) +{ + copy_or_cut_clipboard (name_selector_entry, FALSE); + g_signal_stop_emission_by_name (entry, "copy-clipboard"); +} + +static void +cut_clipboard (GtkEntry *entry, + ENameSelectorEntry *name_selector_entry) +{ + copy_or_cut_clipboard (name_selector_entry, TRUE); + g_signal_stop_emission_by_name (entry, "cut-clipboard"); +} + +static void +e_name_selector_entry_init (ENameSelectorEntry *name_selector_entry) +{ + GtkCellRenderer *renderer; + + name_selector_entry->priv = + E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry); + + g_queue_init (&name_selector_entry->priv->cancellables); + + name_selector_entry->priv->minimum_query_length = 3; + name_selector_entry->priv->show_address = FALSE; + + /* Edit signals */ + + g_signal_connect ( + name_selector_entry, "insert-text", + G_CALLBACK (user_insert_text), name_selector_entry); + g_signal_connect ( + name_selector_entry, "delete-text", + G_CALLBACK (user_delete_text), name_selector_entry); + g_signal_connect ( + name_selector_entry, "focus-out-event", + G_CALLBACK (user_focus_out), name_selector_entry); + g_signal_connect_after ( + name_selector_entry, "focus-in-event", + G_CALLBACK (user_focus_in), name_selector_entry); + + /* Drawing */ + + g_signal_connect ( + name_selector_entry, "draw", + G_CALLBACK (draw_event), name_selector_entry); + + /* Activation: Complete current entry if possible */ + + g_signal_connect ( + name_selector_entry, "activate", + G_CALLBACK (entry_activate), name_selector_entry); + + /* Pop-up menu */ + + g_signal_connect ( + name_selector_entry, "button-press-event", + G_CALLBACK (prepare_popup_destination), name_selector_entry); + g_signal_connect ( + name_selector_entry, "populate-popup", + G_CALLBACK (populate_popup), name_selector_entry); + + /* Clipboard signals */ + g_signal_connect ( + name_selector_entry, "copy-clipboard", + G_CALLBACK (copy_clipboard), name_selector_entry); + g_signal_connect ( + name_selector_entry, "cut-clipboard", + G_CALLBACK (cut_clipboard), name_selector_entry); + + /* Completion */ + + name_selector_entry->priv->email_generator = NULL; + + name_selector_entry->priv->entry_completion = gtk_entry_completion_new (); + gtk_entry_completion_set_match_func ( + name_selector_entry->priv->entry_completion, + (GtkEntryCompletionMatchFunc) completion_match_cb, NULL, NULL); + g_signal_connect_swapped ( + name_selector_entry->priv->entry_completion, "match-selected", + G_CALLBACK (completion_match_selected), name_selector_entry); + + gtk_entry_set_completion ( + GTK_ENTRY (name_selector_entry), + name_selector_entry->priv->entry_completion); + + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (name_selector_entry->priv->entry_completion), + renderer, FALSE); + gtk_cell_layout_set_cell_data_func ( + GTK_CELL_LAYOUT (name_selector_entry->priv->entry_completion), + GTK_CELL_RENDERER (renderer), + (GtkCellLayoutDataFunc) contact_layout_pixbuffer, + name_selector_entry, NULL); + + /* Completion list name renderer */ + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (name_selector_entry->priv->entry_completion), + renderer, TRUE); + gtk_cell_layout_set_cell_data_func ( + GTK_CELL_LAYOUT (name_selector_entry->priv->entry_completion), + GTK_CELL_RENDERER (renderer), + (GtkCellLayoutDataFunc) contact_layout_formatter, + name_selector_entry, NULL); + + /* Destination store */ + + name_selector_entry->priv->destination_store = e_destination_store_new (); + setup_destination_store (name_selector_entry); + name_selector_entry->priv->is_completing = FALSE; +} + +/** + * e_name_selector_entry_new: + * + * Creates a new #ENameSelectorEntry. + * + * Returns: A new #ENameSelectorEntry. + **/ +ENameSelectorEntry * +e_name_selector_entry_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_NAME_SELECTOR_ENTRY, + "registry", registry, NULL); +} + +/** + * e_name_selector_entry_get_registry: + * @name_selector_entry: an #ENameSelectorEntry + * + * Returns the #ESourceRegistry used to query address books. + * + * Returns: the #ESourceRegistry, or %NULL + * + * Since: 3.6 + **/ +ESourceRegistry * +e_name_selector_entry_get_registry (ENameSelectorEntry *name_selector_entry) +{ + g_return_val_if_fail ( + E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), NULL); + + return name_selector_entry->priv->registry; +} + +/** + * e_name_selector_entry_set_registry: + * @name_selector_entry: an #ENameSelectorEntry + * @registry: an #ESourceRegistry + * + * Sets the #ESourceRegistry used to query address books. + * + * This function is intended for cases where @name_selector_entry is + * instantiated by a #GtkBuilder and has to be given an #EsourceRegistry + * after it is fully constructed. + * + * Since: 3.6 + **/ +void +e_name_selector_entry_set_registry (ENameSelectorEntry *name_selector_entry, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry)); + + if (name_selector_entry->priv->registry == registry) + return; + + if (registry != NULL) { + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_object_ref (registry); + } + + if (name_selector_entry->priv->registry != NULL) + g_object_unref (name_selector_entry->priv->registry); + + name_selector_entry->priv->registry = registry; + + g_object_notify (G_OBJECT (name_selector_entry), "registry"); +} + +/** + * e_name_selector_entry_get_minimum_query_length: + * @name_selector_entry: an #ENameSelectorEntry + * + * Returns: Minimum length of query before completion starts + * + * Since: 3.6 + **/ +gint +e_name_selector_entry_get_minimum_query_length (ENameSelectorEntry *name_selector_entry) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), -1); + + return name_selector_entry->priv->minimum_query_length; +} + +/** + * e_name_selector_entry_set_minimum_query_length: + * @name_selector_entry: an #ENameSelectorEntry + * @length: minimum query length + * + * Sets minimum length of query before completion starts. + * + * Since: 3.6 + **/ +void +e_name_selector_entry_set_minimum_query_length (ENameSelectorEntry *name_selector_entry, + gint length) +{ + g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry)); + g_return_if_fail (length > 0); + + if (name_selector_entry->priv->minimum_query_length == length) + return; + + name_selector_entry->priv->minimum_query_length = length; + + g_object_notify (G_OBJECT (name_selector_entry), "minimum-query-length"); +} + +/** + * e_name_selector_entry_get_show_address: + * @name_selector_entry: an #ENameSelectorEntry + * + * Returns: Whether always show email address for an auto-completed contact. + * + * Since: 3.6 + **/ +gboolean +e_name_selector_entry_get_show_address (ENameSelectorEntry *name_selector_entry) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), FALSE); + + return name_selector_entry->priv->show_address; +} + +/** + * e_name_selector_entry_set_show_address: + * @name_selector_entry: an #ENameSelectorEntry + * @show: new value to set + * + * Sets whether always show email address for an auto-completed contact. + * + * Since: 3.6 + **/ +void +e_name_selector_entry_set_show_address (ENameSelectorEntry *name_selector_entry, + gboolean show) +{ + g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry)); + + if ((name_selector_entry->priv->show_address ? 1 : 0) == (show ? 1 : 0)) + return; + + name_selector_entry->priv->show_address = show; + + sanitize_entry (name_selector_entry); + + g_object_notify (G_OBJECT (name_selector_entry), "show-address"); +} + +/** + * e_name_selector_entry_peek_contact_store: + * @name_selector_entry: an #ENameSelectorEntry + * + * Gets the #EContactStore being used by @name_selector_entry. + * + * Returns: An #EContactStore. + **/ +EContactStore * +e_name_selector_entry_peek_contact_store (ENameSelectorEntry *name_selector_entry) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), NULL); + + return name_selector_entry->priv->contact_store; +} + +/** + * e_name_selector_entry_set_contact_store: + * @name_selector_entry: an #ENameSelectorEntry + * @contact_store: an #EContactStore to use + * + * Sets the #EContactStore being used by @name_selector_entry to @contact_store. + **/ +void +e_name_selector_entry_set_contact_store (ENameSelectorEntry *name_selector_entry, + EContactStore *contact_store) +{ + g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry)); + g_return_if_fail (contact_store == NULL || E_IS_CONTACT_STORE (contact_store)); + + if (contact_store == name_selector_entry->priv->contact_store) + return; + + if (name_selector_entry->priv->contact_store) + g_object_unref (name_selector_entry->priv->contact_store); + name_selector_entry->priv->contact_store = contact_store; + if (name_selector_entry->priv->contact_store) + g_object_ref (name_selector_entry->priv->contact_store); + + setup_contact_store (name_selector_entry); +} + +/** + * e_name_selector_entry_peek_destination_store: + * @name_selector_entry: an #ENameSelectorEntry + * + * Gets the #EDestinationStore being used to store @name_selector_entry's destinations. + * + * Returns: An #EDestinationStore. + **/ +EDestinationStore * +e_name_selector_entry_peek_destination_store (ENameSelectorEntry *name_selector_entry) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), NULL); + + return name_selector_entry->priv->destination_store; +} + +/** + * e_name_selector_entry_set_destination_store: + * @name_selector_entry: an #ENameSelectorEntry + * @destination_store: an #EDestinationStore to use + * + * Sets @destination_store as the #EDestinationStore to be used to store + * destinations for @name_selector_entry. + **/ +void +e_name_selector_entry_set_destination_store (ENameSelectorEntry *name_selector_entry, + EDestinationStore *destination_store) +{ + g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry)); + g_return_if_fail (E_IS_DESTINATION_STORE (destination_store)); + + if (destination_store == name_selector_entry->priv->destination_store) + return; + + g_object_unref (name_selector_entry->priv->destination_store); + name_selector_entry->priv->destination_store = g_object_ref (destination_store); + + setup_destination_store (name_selector_entry); +} + +/** + * e_name_selector_entry_get_popup_destination: + * + * Since: 2.32 + **/ +EDestination * +e_name_selector_entry_get_popup_destination (ENameSelectorEntry *name_selector_entry) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), NULL); + + return name_selector_entry->priv->popup_destination; +} + +/** + * e_name_selector_entry_set_contact_editor_func: + * + * DO NOT USE. + **/ +void +e_name_selector_entry_set_contact_editor_func (ENameSelectorEntry *name_selector_entry, + gpointer func) +{ + name_selector_entry->priv->contact_editor_func = func; +} + +/** + * e_name_selector_entry_set_contact_list_editor_func: + * + * DO NOT USE. + **/ +void +e_name_selector_entry_set_contact_list_editor_func (ENameSelectorEntry *name_selector_entry, + gpointer func) +{ + name_selector_entry->priv->contact_list_editor_func = func; +} diff --git a/e-util/e-name-selector-entry.h b/e-util/e-name-selector-entry.h new file mode 100644 index 0000000000..63ce9aa437 --- /dev/null +++ b/e-util/e-name-selector-entry.h @@ -0,0 +1,124 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-name-selector-entry.c - Single-line text entry widget for EDestinations. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_NAME_SELECTOR_ENTRY_H +#define E_NAME_SELECTOR_ENTRY_H + +#include <gtk/gtk.h> +#include <libebook/libebook.h> + +#include <e-util/e-contact-store.h> +#include <e-util/e-destination-store.h> +#include <e-util/e-tree-model-generator.h> + +/* Standard GObject macros */ +#define E_TYPE_NAME_SELECTOR_ENTRY \ + (e_name_selector_entry_get_type ()) +#define E_NAME_SELECTOR_ENTRY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_NAME_SELECTOR_ENTRY, ENameSelectorEntry)) +#define E_NAME_SELECTOR_ENTRY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_NAME_SELECTOR_ENTRY, ENameSelectorEntryClass)) +#define E_IS_NAME_SELECTOR_ENTRY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_NAME_SELECTOR_ENTRY)) +#define E_IS_NAME_SELECTOR_ENTRY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_NAME_SELECTOR_ENTRY)) +#define E_NAME_SELECTOR_ENTRY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_NAME_SELECTOR_ENTRY, ENameSelectorEntryClass)) + +G_BEGIN_DECLS + +typedef struct _ENameSelectorEntry ENameSelectorEntry; +typedef struct _ENameSelectorEntryClass ENameSelectorEntryClass; +typedef struct _ENameSelectorEntryPrivate ENameSelectorEntryPrivate; + +struct _ENameSelectorEntry { + GtkEntry parent; + ENameSelectorEntryPrivate *priv; +}; + +struct _ENameSelectorEntryClass { + GtkEntryClass parent_class; + + void (*updated) (ENameSelectorEntry *entry, gchar *email); + + gpointer reserved1; + gpointer reserved2; +}; + +GType e_name_selector_entry_get_type (void); +ENameSelectorEntry * + e_name_selector_entry_new (ESourceRegistry *registry); +ESourceRegistry * + e_name_selector_entry_get_registry + (ENameSelectorEntry *name_selector_entry); +void e_name_selector_entry_set_registry + (ENameSelectorEntry *name_selector_entry, + ESourceRegistry *registry); +gint e_name_selector_entry_get_minimum_query_length + (ENameSelectorEntry *name_selector_entry); +void e_name_selector_entry_set_minimum_query_length + (ENameSelectorEntry *name_selector_entry, + gint length); +gboolean e_name_selector_entry_get_show_address + (ENameSelectorEntry *name_selector_entry); +void e_name_selector_entry_set_show_address + (ENameSelectorEntry *name_selector_entry, + gboolean show); +EContactStore * e_name_selector_entry_peek_contact_store + (ENameSelectorEntry *name_selector_entry); +void e_name_selector_entry_set_contact_store + (ENameSelectorEntry *name_selector_entry, + EContactStore *contact_store); +EDestinationStore * + e_name_selector_entry_peek_destination_store + (ENameSelectorEntry *name_selector_entry); +void e_name_selector_entry_set_destination_store + (ENameSelectorEntry *name_selector_entry, + EDestinationStore *destination_store); +EDestination * e_name_selector_entry_get_popup_destination + (ENameSelectorEntry *name_selector_entry); + +/* TEMPORARY API - DO NOT USE */ +void e_name_selector_entry_set_contact_editor_func + (ENameSelectorEntry *name_selector_entry, + gpointer func); +void e_name_selector_entry_set_contact_list_editor_func + (ENameSelectorEntry *name_selector_entry, + gpointer func); +gchar * ens_util_populate_user_query_fields + (GSList *user_query_fields, + const gchar *cue_str, + const gchar *encoded_cue_str); + +G_END_DECLS + +#endif /* E_NAME_SELECTOR_ENTRY_H */ diff --git a/e-util/e-name-selector-list.c b/e-util/e-name-selector-list.c new file mode 100644 index 0000000000..67afb504b3 --- /dev/null +++ b/e-util/e-name-selector-list.c @@ -0,0 +1,790 @@ +/* + * Single-line text entry widget for EDestinations. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Srinivasa Ragavan <sragavan@novell.com> + * Devashish Sharma <sdevashish@novell.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#include <config.h> +#include <string.h> +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n-lib.h> + +#include "e-name-selector-list.h" +#include "e-name-selector-entry.h" + +#define E_NAME_SELECTOR_LIST_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_NAME_SELECTOR_LIST, ENameSelectorListPrivate)) + +#define MAX_ROW 10 + +struct _ENameSelectorListPrivate { + GtkWindow *popup; + GtkWidget *tree_view; + GtkWidget *menu; + gint rows; + GdkDevice *grab_keyboard; + GdkDevice *grab_pointer; +}; + +G_DEFINE_TYPE (ENameSelectorList, e_name_selector_list, E_TYPE_NAME_SELECTOR_ENTRY) + +/* Signals */ + +static void +enl_popup_size (ENameSelectorList *list) +{ + gint height = 0, count; + GtkAllocation allocation; + GtkTreeViewColumn *column = NULL; + + column = gtk_tree_view_get_column ( GTK_TREE_VIEW (list->priv->tree_view), 0); + if (column) + gtk_tree_view_column_cell_get_size (column, NULL, NULL, NULL, NULL, &height); + + /* Show a maximum of 10 rows in the popup list view */ + count = list->priv->rows; + if (count > MAX_ROW) + count = MAX_ROW; + if (count <= 0) + count = 1; + + gtk_widget_get_allocation (GTK_WIDGET (list), &allocation); + gtk_widget_set_size_request (list->priv->tree_view, allocation.width - 3 , height * count); +} + +static void +enl_popup_position (ENameSelectorList *list) +{ + GtkAllocation allocation; + GdkWindow *window; + gint x,y; + + gtk_widget_get_allocation (GTK_WIDGET (list), &allocation); + + enl_popup_size (list); + window = gtk_widget_get_window (GTK_WIDGET (list)); + gdk_window_get_origin (window, &x, &y); + y = y + allocation.height; + + gtk_window_move (list->priv->popup, x, y); +} + +static gboolean +popup_grab_on_window (GdkWindow *window, + GdkDevice *keyboard, + GdkDevice *pointer, + guint32 activate_time) +{ + if (keyboard && gdk_device_grab (keyboard, window, + GDK_OWNERSHIP_WINDOW, TRUE, + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK, + NULL, activate_time) != GDK_GRAB_SUCCESS) + return FALSE; + + if (pointer && gdk_device_grab (pointer, window, + GDK_OWNERSHIP_WINDOW, TRUE, + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK, + NULL, activate_time) != GDK_GRAB_SUCCESS) { + if (keyboard) + gdk_device_ungrab (keyboard, activate_time); + + return FALSE; + } + + return TRUE; +} + +static void +enl_popup_grab (ENameSelectorList *list, + const GdkEvent *event) +{ + EDestinationStore *store; + ENameSelectorEntry *entry; + GdkWindow *window; + GdkDevice *device = NULL; + GdkDevice *keyboard, *pointer; + gint len; + + if (list->priv->grab_pointer && list->priv->grab_keyboard) + return; + + window = gtk_widget_get_window (GTK_WIDGET (list->priv->popup)); + + if (event) + device = gdk_event_get_device (event); + if (!device) + device = gtk_get_current_event_device (); + if (!device) { + GdkDeviceManager *device_manager; + + device_manager = gdk_display_get_device_manager (gtk_widget_get_display (GTK_WIDGET (list))); + device = gdk_device_manager_get_client_pointer (device_manager); + } + + if (gdk_device_get_source (device) == GDK_SOURCE_KEYBOARD) { + keyboard = device; + pointer = gdk_device_get_associated_device (device); + } else { + pointer = device; + keyboard = gdk_device_get_associated_device (device); + } + + if (!popup_grab_on_window (window, keyboard, pointer, gtk_get_current_event_time ())) + return; + + gtk_widget_grab_focus ((GtkWidget *) list); + + /* Build the listview from the model */ + entry = E_NAME_SELECTOR_ENTRY (list); + store = e_name_selector_entry_peek_destination_store (entry); + gtk_tree_view_set_model ( + GTK_TREE_VIEW (list->priv->tree_view), + GTK_TREE_MODEL (store)); + + /* If any selection of text is present, unselect it */ + len = strlen (gtk_entry_get_text (GTK_ENTRY (list))); + gtk_editable_select_region (GTK_EDITABLE (list), len, -1); + + gtk_device_grab_add (GTK_WIDGET (list->priv->popup), pointer, TRUE); + list->priv->grab_keyboard = keyboard; + list->priv->grab_pointer = pointer; +} + +static void +enl_popup_ungrab (ENameSelectorList *list) +{ + if (!list->priv->grab_pointer || + !list->priv->grab_keyboard || + !gtk_widget_has_grab (GTK_WIDGET (list->priv->popup))) + return; + + gtk_device_grab_remove (GTK_WIDGET (list->priv->popup), list->priv->grab_pointer); + gtk_device_grab_remove (GTK_WIDGET (list->priv->popup), list->priv->grab_keyboard); + + list->priv->grab_pointer = NULL; + list->priv->grab_keyboard = NULL; +} + +static gboolean +enl_entry_focus_in (ENameSelectorList *list, + GdkEventFocus *event, + gpointer dummy) +{ + gint len; + + /* FIXME: Dont select every thing by default- Code is there but still it does */ + len = strlen (gtk_entry_get_text (GTK_ENTRY (list))); + gtk_editable_select_region (GTK_EDITABLE (list), len, -1); + + return TRUE; +} + +static gboolean +enl_entry_focus_out (ENameSelectorList *list, + GdkEventFocus *event, + gpointer dummy) +{ + /* When we lose focus and popup is still present hide it. Dont do it, when we click the popup. Look for grab */ + if (gtk_widget_get_visible (GTK_WIDGET (list->priv->popup)) + && !gtk_widget_has_grab (GTK_WIDGET (list->priv->popup))) { + enl_popup_ungrab (list); + gtk_widget_hide ((GtkWidget *) list->priv->popup); + + return FALSE; + } + + return FALSE; +} + +static gboolean +enl_popup_button_press (GtkWidget *widget, + GdkEventButton *event, + ENameSelectorList *list) +{ + if (!gtk_widget_get_mapped (widget)) + return FALSE; + + /* if we come here, it's usually time to popdown */ + gtk_widget_hide ((GtkWidget *) list->priv->popup); + + return TRUE; +} + +static gboolean +enl_popup_focus_out (GtkWidget *w, + GdkEventFocus *event, + ENameSelectorList *list) +{ + /* Just ungrab. We lose focus on button press event */ + enl_popup_ungrab (list); + return TRUE; +} + +static gboolean +enl_popup_enter_notify (GtkWidget *widget, + GdkEventCrossing *event, + ENameSelectorList *list) +{ + if (event->type == GDK_ENTER_NOTIFY && !gtk_widget_has_grab (GTK_WIDGET (list->priv->popup))) + enl_popup_grab (list, (GdkEvent *) event); + + return TRUE; +} + +static void +enl_tree_select_node (ENameSelectorList *list, + gint n) +{ + EDestinationStore *store; + ENameSelectorEntry *entry; + GtkTreeSelection *selection; + GtkTreeViewColumn *column; + GtkTreeView *tree_view; + GtkTreeIter iter; + GtkTreePath *path; + + entry = E_NAME_SELECTOR_ENTRY (list); + tree_view = GTK_TREE_VIEW (list->priv->tree_view); + store = e_name_selector_entry_peek_destination_store (entry); + selection = gtk_tree_view_get_selection (tree_view); + iter.stamp = e_destination_store_get_stamp (store); + iter.user_data = GINT_TO_POINTER (n - 1); + + gtk_tree_selection_unselect_all (selection); + gtk_tree_selection_select_iter (selection, &iter); + + column = gtk_tree_view_get_column (tree_view, 0); + path = e_destination_store_get_path (GTK_TREE_MODEL (store), &iter); + gtk_tree_view_scroll_to_cell (tree_view, path, column, FALSE, 0, 0); + gtk_tree_view_set_cursor (tree_view, path, column, FALSE); + gtk_widget_grab_focus (GTK_WIDGET (tree_view)); + /*Fixme: We should grab the focus to the column. How? */ + + gtk_tree_path_free (path); +} + +static gboolean +enl_entry_key_press_event (ENameSelectorList *list, + GdkEventKey *event, + gpointer dummy) +{ + ENameSelectorEntry *entry; + EDestinationStore *store; + + entry = E_NAME_SELECTOR_ENTRY (list); + store = e_name_selector_entry_peek_destination_store (entry); + + if ( (event->state & GDK_CONTROL_MASK) && (event->keyval == GDK_KEY_Down)) { + enl_popup_position (list); + gtk_widget_show_all (GTK_WIDGET (list->priv->popup)); + enl_popup_grab (list, (GdkEvent *) event); + list->priv->rows = e_destination_store_get_destination_count (store); + enl_popup_size (list); + enl_tree_select_node (list, 1); + return TRUE; + } + return FALSE; +} + +static void +delete_row (GtkTreePath *path, + ENameSelectorList *list) +{ + ENameSelectorEntry *entry; + EDestinationStore *store; + GtkTreeIter iter; + gint n, len; + GtkTreeSelection *selection; + + entry = E_NAME_SELECTOR_ENTRY (list); + store = e_name_selector_entry_peek_destination_store (entry); + + if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, path)) + return; + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (list->priv->tree_view)); + len = e_destination_store_get_destination_count (store); + n = GPOINTER_TO_INT (iter.user_data); + + e_destination_store_remove_destination_nth (store, n); + + /* If the last one is deleted select the last but one or the deleted +1 */ + if (n == len -1) + n -= 1; + + /* We deleted the last entry */ + if (len == 1) { + enl_popup_ungrab (list); + if (list->priv->menu) + gtk_menu_popdown (GTK_MENU (list->priv->menu)); + gtk_widget_hide (GTK_WIDGET (list->priv->popup)); + return; + } + + iter.stamp = e_destination_store_get_stamp (store); + iter.user_data = GINT_TO_POINTER (n); + + gtk_tree_selection_unselect_all (selection); + gtk_tree_selection_select_iter (selection, &iter); + + gtk_tree_path_free (path); + + list->priv->rows = e_destination_store_get_destination_count (store); + enl_popup_size (list); +} + +static void +popup_activate_email (ENameSelectorEntry *name_selector_entry, + GtkWidget *menu_item) +{ + EDestination *destination; + EContact *contact; + gint email_num; + + destination = e_name_selector_entry_get_popup_destination (name_selector_entry); + if (!destination) + return; + + contact = e_destination_get_contact (destination); + if (!contact) + return; + + email_num = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (menu_item), "order")); + e_destination_set_contact (destination, contact, email_num); +} + +static void +popup_activate_list (EDestination *destination, + GtkWidget *item) +{ + gboolean status = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)); + + e_destination_set_ignored (destination, !status); +} + +static void +destination_set_list (GtkWidget *item, + EDestination *destination) +{ + EContact *contact; + gboolean status = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)); + + contact = e_destination_get_contact (destination); + if (!contact) + return; + + e_destination_set_ignored (destination, !status); +} + +static void +destination_set_email (GtkWidget *item, + EDestination *destination) +{ + gint email_num; + EContact *contact; + + if (!gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item))) + return; + contact = e_destination_get_contact (destination); + if (!contact) + return; + + email_num = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "order")); + e_destination_set_contact (destination, contact, email_num); +} + +typedef struct { + ENameSelectorList *list; + GtkTreePath *path; +}PopupDeleteRowInfo; + +static void +popup_delete_row (GtkWidget *w, + PopupDeleteRowInfo *row_info) +{ + delete_row (row_info->path, row_info->list); + g_free (row_info); +} + +static void +menu_deactivate (GtkMenuShell *junk, + ENameSelectorList *list) +{ + enl_popup_grab (list, NULL); +} + +static gboolean +enl_tree_button_press_event (GtkWidget *widget, + GdkEventButton *event, + ENameSelectorList *list) +{ + GtkWidget *menu; + EDestination *destination; + ENameSelectorEntry *entry; + EDestinationStore *store; + EContact *contact; + GtkWidget *menu_item; + GList *email_list = NULL, *l; + gint i; + gint email_num, len; + gchar *delete_label; + GSList *group = NULL; + gboolean is_list; + gboolean show_menu = FALSE; + GtkTreeSelection *selection; + GtkTreeView *tree_view; + GtkTreePath *path; + PopupDeleteRowInfo *row_info; + GtkTreeIter iter; + + entry = E_NAME_SELECTOR_ENTRY (list); + tree_view = GTK_TREE_VIEW (list->priv->tree_view); + store = e_name_selector_entry_peek_destination_store (entry); + + if (!gtk_widget_has_grab (GTK_WIDGET (list->priv->popup))) + enl_popup_grab (list, (GdkEvent *) event); + + gtk_tree_view_get_dest_row_at_pos ( + tree_view, event->x, event->y, &path, NULL); + selection = gtk_tree_view_get_selection (tree_view); + if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, path)) + return FALSE; + + gtk_tree_selection_unselect_all (selection); + gtk_tree_selection_select_iter (selection, &iter); + + if (event->button != 3) { + return FALSE; + } + + destination = e_destination_store_get_destination (store, &iter); + + if (!destination) + return FALSE; + + contact = e_destination_get_contact (destination); + if (!contact) + return FALSE; + + if (list->priv->menu) { + gtk_menu_popdown (GTK_MENU (list->priv->menu)); + } + menu = gtk_menu_new (); + g_signal_connect (menu, "deactivate", G_CALLBACK (menu_deactivate), list); + list->priv->menu = menu; + gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, event->button, gtk_get_current_event_time ()); + + email_num = e_destination_get_email_num (destination); + + /* Addresses */ + is_list = e_contact_get (contact, E_CONTACT_IS_LIST) ? TRUE : FALSE; + if (is_list) { + const GList *dests = e_destination_list_get_dests (destination); + GList *iters; + gint length = g_list_length ((GList *) dests); + + for (iters = (GList *) dests; iters; iters = iters->next) { + EDestination *dest = (EDestination *) iters->data; + const gchar *email = e_destination_get_email (dest); + + if (!email || *email == '\0') + continue; + + if (length > 1) { + menu_item = gtk_check_menu_item_new_with_label (email); + g_signal_connect ( + menu_item, "toggled", + G_CALLBACK (destination_set_list), dest); + } else { + menu_item = gtk_menu_item_new_with_label (email); + } + + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + show_menu = TRUE; + + if (length > 1) { + gtk_check_menu_item_set_active ( + GTK_CHECK_MENU_ITEM (menu_item), + !e_destination_is_ignored (dest)); + g_signal_connect_swapped ( + menu_item, "activate", + G_CALLBACK (popup_activate_list), dest); + } + } + + } else { + email_list = e_contact_get (contact, E_CONTACT_EMAIL); + len = g_list_length (email_list); + + for (l = email_list, i = 0; l; l = g_list_next (l), i++) { + gchar *email = l->data; + + if (!email || *email == '\0') + continue; + + if (len > 1) { + menu_item = gtk_radio_menu_item_new_with_label (group, email); + group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (menu_item)); + g_signal_connect ( + menu_item, "toggled", + G_CALLBACK (destination_set_email), + destination); + } else { + menu_item = gtk_menu_item_new_with_label (email); + } + + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + show_menu = TRUE; + g_object_set_data (G_OBJECT (menu_item), "order", GINT_TO_POINTER (i)); + + if (i == email_num && len > 1) { + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menu_item), TRUE); + g_signal_connect_swapped ( + menu_item, "activate", + G_CALLBACK (popup_activate_email), + entry); + } + } + g_list_foreach (email_list, (GFunc) g_free, NULL); + g_list_free (email_list); + } + + /* Separator */ + + if (show_menu) { + menu_item = gtk_separator_menu_item_new (); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + } + + delete_label = g_strdup_printf (_("_Delete %s"), (gchar *) e_contact_get_const (contact, E_CONTACT_FILE_AS)); + menu_item = gtk_menu_item_new_with_mnemonic (delete_label); + g_free (delete_label); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item); + + row_info = g_new (PopupDeleteRowInfo, 1); + row_info->list = list; + row_info->path = path; + + g_signal_connect ( + menu_item, "activate", + G_CALLBACK (popup_delete_row), row_info); + + return TRUE; + +} + +static gboolean +enl_tree_key_press_event (GtkWidget *w, + GdkEventKey *event, + ENameSelectorList *list) +{ + if (event->keyval == GDK_KEY_Escape) { + enl_popup_ungrab (list); + gtk_widget_hide ( GTK_WIDGET (list->priv->popup)); + return TRUE; + } else if (event->keyval == GDK_KEY_Delete) { + GtkTreeSelection *selection; + GtkTreeView *tree_view; + GList *paths; + + tree_view = GTK_TREE_VIEW (list->priv->tree_view); + selection = gtk_tree_view_get_selection (tree_view); + paths = gtk_tree_selection_get_selected_rows (selection, NULL); + paths = g_list_reverse (paths); + g_list_foreach (paths, (GFunc) delete_row, list); + g_list_free (paths); + } else if (event->keyval != GDK_KEY_Up && event->keyval != GDK_KEY_Down + && event->keyval != GDK_KEY_Shift_R && event->keyval != GDK_KEY_Shift_L + && event->keyval != GDK_KEY_Control_R && event->keyval != GDK_KEY_Control_L) { + enl_popup_ungrab (list); + gtk_widget_hide ( GTK_WIDGET (list->priv->popup)); + gtk_widget_event (GTK_WIDGET (list), (GdkEvent *) event); + return TRUE; + } + + return FALSE; +} + +void +e_name_selector_list_expand_clicked (ENameSelectorList *list) +{ + ENameSelectorEntry *entry; + EDestinationStore *store; + + entry = E_NAME_SELECTOR_ENTRY (list); + store = e_name_selector_entry_peek_destination_store (entry); + + if (!gtk_widget_get_visible (GTK_WIDGET (list->priv->popup))) { + enl_popup_position (list); + gtk_widget_show_all (GTK_WIDGET (list->priv->popup)); + enl_popup_grab (list, NULL); + list->priv->rows = e_destination_store_get_destination_count (store); + enl_popup_size (list); + enl_tree_select_node (list, 1); + } + else { + enl_popup_ungrab (list); + if (list->priv->menu) + gtk_menu_popdown (GTK_MENU (list->priv->menu)); + gtk_widget_hide (GTK_WIDGET (list->priv->popup)); + } +} + +static void +name_selector_list_realize (GtkWidget *widget) +{ + ENameSelectorList *list; + ENameSelectorEntry *entry; + EDestinationStore *store; + + /* Chain up to parent's realize() method. */ + GTK_WIDGET_CLASS (e_name_selector_list_parent_class)->realize (widget); + + list = E_NAME_SELECTOR_LIST (widget); + entry = E_NAME_SELECTOR_ENTRY (widget); + store = e_name_selector_entry_peek_destination_store (entry); + + gtk_tree_view_set_model ( + GTK_TREE_VIEW (list->priv->tree_view), GTK_TREE_MODEL (store)); +} + +static void +e_name_selector_list_class_init (ENameSelectorListClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (ENameSelectorListPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->realize = name_selector_list_realize; +} + +static void +e_name_selector_list_init (ENameSelectorList *list) +{ + GtkCellRenderer *renderer; + GtkWidget *scroll, *popup_frame, *vgrid; + GtkTreeSelection *selection; + GtkTreeViewColumn *column; + ENameSelectorEntry *entry; + EDestinationStore *store; + GtkEntryCompletion *completion; + + list->priv = E_NAME_SELECTOR_LIST_GET_PRIVATE (list); + list->priv->menu = NULL; + + entry = E_NAME_SELECTOR_ENTRY (list); + store = e_name_selector_entry_peek_destination_store (entry); + + list->priv->tree_view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store)); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (list->priv->tree_view), FALSE); + gtk_tree_view_set_hover_selection (GTK_TREE_VIEW (list->priv->tree_view), FALSE); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (list->priv->tree_view)); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE); + gtk_tree_selection_unselect_all (selection); + gtk_tree_view_set_enable_search (GTK_TREE_VIEW (list->priv->tree_view), FALSE); + + completion = gtk_entry_get_completion (GTK_ENTRY (list)); + gtk_entry_completion_set_inline_completion (completion, TRUE); + gtk_entry_completion_set_popup_completion (completion, TRUE); + + renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes ("Name", renderer, "text", E_DESTINATION_STORE_COLUMN_ADDRESS, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (list->priv->tree_view), column); + gtk_tree_view_column_set_clickable (column, TRUE); + + scroll = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_NONE); + gtk_widget_set_size_request ( + gtk_scrolled_window_get_vscrollbar ( + GTK_SCROLLED_WINDOW (scroll)), -1, 0); + gtk_widget_set_vexpand (scroll, TRUE); + gtk_widget_set_valign (scroll, GTK_ALIGN_FILL); + + list->priv->popup = GTK_WINDOW (gtk_window_new (GTK_WINDOW_POPUP)); + gtk_window_set_resizable (GTK_WINDOW (list->priv->popup), FALSE); + + popup_frame = gtk_frame_new (NULL); + gtk_frame_set_shadow_type ( + GTK_FRAME (popup_frame), GTK_SHADOW_ETCHED_IN); + + gtk_container_add (GTK_CONTAINER (list->priv->popup), popup_frame); + + vgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_VERTICAL, + "column-homogeneous", FALSE, + "row-spacing", 0, + NULL); + gtk_container_add (GTK_CONTAINER (popup_frame), vgrid); + + gtk_container_add (GTK_CONTAINER (scroll), list->priv->tree_view); + gtk_container_add (GTK_CONTAINER (vgrid), scroll); + + g_signal_connect_after ( + GTK_WIDGET (list), "focus-in-event", + G_CALLBACK (enl_entry_focus_in), NULL); + g_signal_connect ( + GTK_WIDGET (list), "focus-out-event", + G_CALLBACK (enl_entry_focus_out), NULL); + g_signal_connect ( + GTK_WIDGET (list), "key-press-event", + G_CALLBACK (enl_entry_key_press_event), NULL); + + g_signal_connect_after ( + list->priv->tree_view, "key-press-event", + G_CALLBACK (enl_tree_key_press_event), list); + g_signal_connect ( + list->priv->tree_view, "button-press-event", + G_CALLBACK (enl_tree_button_press_event), list); + + g_signal_connect ( + list->priv->popup, "button-press-event", + G_CALLBACK (enl_popup_button_press), list); + g_signal_connect ( + list->priv->popup, "focus-out-event", + G_CALLBACK (enl_popup_focus_out), list); + g_signal_connect ( + list->priv->popup, "enter-notify-event", + G_CALLBACK (enl_popup_enter_notify), list); + +} + +ENameSelectorList * +e_name_selector_list_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_NAME_SELECTOR_LIST, + "registry", registry, NULL); +} diff --git a/e-util/e-name-selector-list.h b/e-util/e-name-selector-list.h new file mode 100644 index 0000000000..7b1d11c0c6 --- /dev/null +++ b/e-util/e-name-selector-list.h @@ -0,0 +1,82 @@ +/* + * Single-line text entry widget for EDestinations. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Srinivasa Ragavan <sragavan@novell.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_NAME_SELECTOR_LIST_H +#define E_NAME_SELECTOR_LIST_H + +#include <gtk/gtk.h> +#include <libebook/libebook.h> + +#include <e-util/e-contact-store.h> +#include <e-util/e-destination-store.h> +#include <e-util/e-tree-model-generator.h> +#include <e-util/e-name-selector-entry.h> + +/* Standard GObject macros */ +#define E_TYPE_NAME_SELECTOR_LIST \ + (e_name_selector_list_get_type ()) +#define E_NAME_SELECTOR_LIST(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_NAME_SELECTOR_LIST, ENameSelectorList)) +#define E_NAME_SELECTOR_LIST_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_NAME_SELECTOR_LIST, ENameSelectorListClass)) +#define E_IS_NAME_SELECTOR_LIST(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_NAME_SELECTOR_LIST)) +#define E_IS_NAME_SELECTOR_LIST_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_NAME_SELECTOR_LIST)) +#define E_NAME_SELECTOR_LIST_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_NAME_SELECTOR_LIST, ENameSelectorListClass)) + +G_BEGIN_DECLS + +typedef struct _ENameSelectorList ENameSelectorList; +typedef struct _ENameSelectorListClass ENameSelectorListClass; +typedef struct _ENameSelectorListPrivate ENameSelectorListPrivate; + +struct _ENameSelectorList { + ENameSelectorEntry parent; + ENameSelectorListPrivate *priv; +}; + +struct _ENameSelectorListClass { + ENameSelectorEntryClass parent_class; +}; + +GType e_name_selector_list_get_type (void); +ENameSelectorList * + e_name_selector_list_new (ESourceRegistry *registry); +void e_name_selector_list_expand_clicked + (ENameSelectorList *list); + +G_END_DECLS + +#endif /* E_NAME_SELECTOR_LIST_H */ diff --git a/e-util/e-name-selector-model.c b/e-util/e-name-selector-model.c new file mode 100644 index 0000000000..770f51422f --- /dev/null +++ b/e-util/e-name-selector-model.c @@ -0,0 +1,663 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-name-selector-model.c - Model for contact selection. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <string.h> +#include <glib/gi18n-lib.h> +#include "e-name-selector-model.h" + +#define E_NAME_SELECTOR_MODEL_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_NAME_SELECTOR_MODEL, ENameSelectorModelPrivate)) + +typedef struct { + gchar *name; + gchar *pretty_name; + + EDestinationStore *destination_store; +} +Section; + +struct _ENameSelectorModelPrivate { + GArray *sections; + EContactStore *contact_store; + ETreeModelGenerator *contact_filter; + GHashTable *destination_uid_hash; +}; + +static gint generate_contact_rows (EContactStore *contact_store, GtkTreeIter *iter, + ENameSelectorModel *name_selector_model); +static void override_email_address (EContactStore *contact_store, GtkTreeIter *iter, + gint permutation_n, gint column, GValue *value, + ENameSelectorModel *name_selector_model); +static void free_section (ENameSelectorModel *name_selector_model, gint n); + +/* ------------------ * + * Class/object setup * + * ------------------ */ + +/* Signals */ + +enum { + SECTION_ADDED, + SECTION_REMOVED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (ENameSelectorModel, e_name_selector_model, G_TYPE_OBJECT) + +static void +e_name_selector_model_init (ENameSelectorModel *name_selector_model) +{ + name_selector_model->priv = + E_NAME_SELECTOR_MODEL_GET_PRIVATE (name_selector_model); + + name_selector_model->priv->sections = g_array_new (FALSE, FALSE, sizeof (Section)); + name_selector_model->priv->contact_store = e_contact_store_new (); + + name_selector_model->priv->contact_filter = + e_tree_model_generator_new (GTK_TREE_MODEL (name_selector_model->priv->contact_store)); + e_tree_model_generator_set_generate_func ( + name_selector_model->priv->contact_filter, + (ETreeModelGeneratorGenerateFunc) generate_contact_rows, + name_selector_model, NULL); + e_tree_model_generator_set_modify_func (name_selector_model->priv->contact_filter, + (ETreeModelGeneratorModifyFunc) override_email_address, + name_selector_model, NULL); + + g_object_unref (name_selector_model->priv->contact_store); + + name_selector_model->priv->destination_uid_hash = NULL; +} + +static void +name_selector_model_finalize (GObject *object) +{ + ENameSelectorModelPrivate *priv; + gint i; + + priv = E_NAME_SELECTOR_MODEL_GET_PRIVATE (object); + + for (i = 0; i < priv->sections->len; i++) + free_section (E_NAME_SELECTOR_MODEL (object), i); + + g_array_free (priv->sections, TRUE); + g_object_unref (priv->contact_filter); + + if (priv->destination_uid_hash) + g_hash_table_destroy (priv->destination_uid_hash); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_name_selector_model_parent_class)->finalize (object); +} + +static void +e_name_selector_model_class_init (ENameSelectorModelClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ENameSelectorModelPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = name_selector_model_finalize; + + signals[SECTION_ADDED] = g_signal_new ( + "section-added", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ENameSelectorModelClass, section_added), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + signals[SECTION_REMOVED] = g_signal_new ( + "section-removed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ENameSelectorModelClass, section_removed), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); +} + +/** + * e_name_selector_model_new: + * + * Creates a new #ENameSelectorModel. + * + * Returns: A new #ENameSelectorModel. + **/ +ENameSelectorModel * +e_name_selector_model_new (void) +{ + return E_NAME_SELECTOR_MODEL (g_object_new (E_TYPE_NAME_SELECTOR_MODEL, NULL)); +} + +/* ---------------------------- * + * GtkTreeModelFilter filtering * + * ---------------------------- */ + +static void +deep_free_list (GList *list) +{ + GList *l; + + for (l = list; l; l = g_list_next (l)) + g_free (l->data); + + g_list_free (list); +} + +static gint +generate_contact_rows (EContactStore *contact_store, + GtkTreeIter *iter, + ENameSelectorModel *name_selector_model) +{ + EContact *contact; + const gchar *contact_uid; + gint n_rows, used_rows = 0; + gint i; + + contact = e_contact_store_get_contact (contact_store, iter); + g_assert (contact != NULL); + + contact_uid = e_contact_get_const (contact, E_CONTACT_UID); + if (!contact_uid) + return 0; /* Can happen with broken databases */ + + for (i = 0; i < name_selector_model->priv->sections->len; i++) { + Section *section; + GList *destinations; + GList *l; + + section = &g_array_index (name_selector_model->priv->sections, Section, i); + destinations = e_destination_store_list_destinations (section->destination_store); + + for (l = destinations; l; l = g_list_next (l)) { + EDestination *destination = l->data; + const gchar *destination_uid; + + destination_uid = e_destination_get_contact_uid (destination); + if (destination_uid && !strcmp (contact_uid, destination_uid)) { + used_rows++; + } + } + + g_list_free (destinations); + } + + if (e_contact_get (contact, E_CONTACT_IS_LIST)) { + n_rows = 1 - used_rows; + } else { + GList *email_list; + + email_list = e_contact_get (contact, E_CONTACT_EMAIL); + n_rows = g_list_length (email_list) - used_rows; + deep_free_list (email_list); + } + + g_return_val_if_fail (n_rows >= 0, 0); + + return n_rows; +} + +static void +override_email_address (EContactStore *contact_store, + GtkTreeIter *iter, + gint permutation_n, + gint column, + GValue *value, + ENameSelectorModel *name_selector_model) +{ + if (column == E_CONTACT_EMAIL_1) { + EContact *contact; + GList *email_list; + gchar *email; + + contact = e_contact_store_get_contact (contact_store, iter); + email_list = e_name_selector_model_get_contact_emails_without_used (name_selector_model, contact, TRUE); + g_return_if_fail (g_list_length (email_list) <= permutation_n); + email = g_strdup (g_list_nth_data (email_list, permutation_n)); + g_value_set_string (value, email); + e_name_selector_model_free_emails_list (email_list); + } else { + gtk_tree_model_get_value (GTK_TREE_MODEL (contact_store), iter, column, value); + } +} + +/* --------------- * + * Section helpers * + * --------------- */ + +typedef struct +{ + ENameSelectorModel *name_selector_model; + GHashTable *other_hash; +} +HashCompare; + +static void +emit_destination_uid_changes_cb (gchar *uid_num, + gpointer value, + HashCompare *hash_compare) +{ + EContactStore *contact_store = hash_compare->name_selector_model->priv->contact_store; + + if (!hash_compare->other_hash || !g_hash_table_lookup (hash_compare->other_hash, uid_num)) { + GtkTreeIter iter; + GtkTreePath *path; + gchar *sep; + + sep = strrchr (uid_num, ':'); + g_return_if_fail (sep != NULL); + + *sep = '\0'; + if (e_contact_store_find_contact (contact_store, uid_num, &iter)) { + *sep = ':'; + + path = gtk_tree_model_get_path (GTK_TREE_MODEL (contact_store), &iter); + gtk_tree_model_row_changed (GTK_TREE_MODEL (contact_store), path, &iter); + gtk_tree_path_free (path); + } else { + *sep = ':'; + } + } +} + +static void +destinations_changed (ENameSelectorModel *name_selector_model) +{ + GHashTable *destination_uid_hash_new; + GHashTable *destination_uid_hash_old; + HashCompare hash_compare; + gint i; + + destination_uid_hash_new = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + for (i = 0; i < name_selector_model->priv->sections->len; i++) { + Section *section = &g_array_index (name_selector_model->priv->sections, Section, i); + GList *destinations; + GList *l; + + destinations = e_destination_store_list_destinations (section->destination_store); + + for (l = destinations; l; l = g_list_next (l)) { + EDestination *destination = l->data; + const gchar *destination_uid; + + destination_uid = e_destination_get_contact_uid (destination); + if (destination_uid) + g_hash_table_insert ( + destination_uid_hash_new, + g_strdup_printf ( + "%s:%d", destination_uid, + e_destination_get_email_num (destination)), + GINT_TO_POINTER (TRUE)); + } + + g_list_free (destinations); + } + + destination_uid_hash_old = name_selector_model->priv->destination_uid_hash; + name_selector_model->priv->destination_uid_hash = destination_uid_hash_new; + + hash_compare.name_selector_model = name_selector_model; + + hash_compare.other_hash = destination_uid_hash_old; + g_hash_table_foreach ( + destination_uid_hash_new, + (GHFunc) emit_destination_uid_changes_cb, + &hash_compare); + + if (destination_uid_hash_old) { + hash_compare.other_hash = destination_uid_hash_new; + g_hash_table_foreach ( + destination_uid_hash_old, + (GHFunc) emit_destination_uid_changes_cb, + &hash_compare); + + g_hash_table_destroy (destination_uid_hash_old); + } +} + +static void +free_section (ENameSelectorModel *name_selector_model, + gint n) +{ + Section *section; + + g_assert (n >= 0); + g_assert (n < name_selector_model->priv->sections->len); + + section = &g_array_index (name_selector_model->priv->sections, Section, n); + + g_signal_handlers_disconnect_matched ( + section->destination_store, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, name_selector_model); + + g_free (section->name); + g_free (section->pretty_name); + g_object_unref (section->destination_store); +} + +static gint +find_section_by_name (ENameSelectorModel *name_selector_model, + const gchar *name) +{ + gint i; + + g_assert (name != NULL); + + for (i = 0; i < name_selector_model->priv->sections->len; i++) { + Section *section = &g_array_index (name_selector_model->priv->sections, Section, i); + + if (!strcmp (name, section->name)) + return i; + } + + return -1; +} + +/* ---------------------- * + * ENameSelectorModel API * + * ---------------------- */ + +/** + * e_name_selector_model_peek_contact_store: + * @name_selector_model: an #ENameSelectorModel + * + * Gets the #EContactStore associated with @name_selector_model. + * + * Returns: An #EContactStore. + **/ +EContactStore * +e_name_selector_model_peek_contact_store (ENameSelectorModel *name_selector_model) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR_MODEL (name_selector_model), NULL); + + return name_selector_model->priv->contact_store; +} + +/** + * e_name_selector_model_peek_contact_filter: + * @name_selector_model: an #ENameSelectorModel + * + * Gets the #ETreeModelGenerator being used to filter and/or extend the + * list of contacts in @name_selector_model's #EContactStore. + * + * Returns: An #ETreeModelGenerator. + **/ +ETreeModelGenerator * +e_name_selector_model_peek_contact_filter (ENameSelectorModel *name_selector_model) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR_MODEL (name_selector_model), NULL); + + return name_selector_model->priv->contact_filter; +} + +/** + * e_name_selector_model_list_sections: + * @name_selector_model: an #ENameSelectorModel + * + * Gets a list of the destination sections in @name_selector_model. + * + * Returns: A #GList of pointers to strings. The #GList and the + * strings belong to the caller, and must be freed when no longer needed. + **/ +GList * +e_name_selector_model_list_sections (ENameSelectorModel *name_selector_model) +{ + GList *section_names = NULL; + gint i; + + g_return_val_if_fail (E_IS_NAME_SELECTOR_MODEL (name_selector_model), NULL); + + /* Do this backwards so we can use g_list_prepend () and get correct order */ + for (i = name_selector_model->priv->sections->len - 1; i >= 0; i--) { + Section *section = &g_array_index (name_selector_model->priv->sections, Section, i); + gchar *name; + + name = g_strdup (section->name); + section_names = g_list_prepend (section_names, name); + } + + return section_names; +} + +/** + * e_name_selector_model_add_section: + * @name_selector_model: an #ENameSelectorModel + * @name: internal name of this section + * @pretty_name: user-visible name of this section + * @destination_store: the #EDestinationStore to use to store the destinations for this + * section, or %NULL if @name_selector_model should create its own. + * + * Adds a destination section to @name_selector_model. + **/ +void +e_name_selector_model_add_section (ENameSelectorModel *name_selector_model, + const gchar *name, + const gchar *pretty_name, + EDestinationStore *destination_store) +{ + Section section; + + g_return_if_fail (E_IS_NAME_SELECTOR_MODEL (name_selector_model)); + g_return_if_fail (name != NULL); + g_return_if_fail (pretty_name != NULL); + + if (find_section_by_name (name_selector_model, name) >= 0) { + g_warning ("ENameSelectorModel already has a section called '%s'!", name); + return; + } + + memset (§ion, 0, sizeof (Section)); + + section.name = g_strdup (name); + section.pretty_name = g_strdup (pretty_name); + + if (destination_store) + section.destination_store = g_object_ref (destination_store); + else + section.destination_store = e_destination_store_new (); + + g_signal_connect_swapped ( + section.destination_store, "row-changed", + G_CALLBACK (destinations_changed), name_selector_model); + g_signal_connect_swapped ( + section.destination_store, "row-deleted", + G_CALLBACK (destinations_changed), name_selector_model); + g_signal_connect_swapped ( + section.destination_store, "row-inserted", + G_CALLBACK (destinations_changed), name_selector_model); + + g_array_append_val (name_selector_model->priv->sections, section); + + destinations_changed (name_selector_model); + g_signal_emit (name_selector_model, signals[SECTION_ADDED], 0, name); +} + +/** + * e_name_selector_model_remove_section: + * @name_selector_model: an #ENameSelectorModel + * @name: internal name of the section to remove + * + * Removes a destination section from @name_selector_model. + **/ +void +e_name_selector_model_remove_section (ENameSelectorModel *name_selector_model, + const gchar *name) +{ + gint n; + + g_return_if_fail (E_IS_NAME_SELECTOR_MODEL (name_selector_model)); + g_return_if_fail (name != NULL); + + n = find_section_by_name (name_selector_model, name); + if (n < 0) { + g_warning ("ENameSelectorModel does not have a section called '%s'!", name); + return; + } + + free_section (name_selector_model, n); + g_array_remove_index_fast (name_selector_model->priv->sections, n); /* Order doesn't matter */ + + destinations_changed (name_selector_model); + g_signal_emit (name_selector_model, signals[SECTION_REMOVED], 0, name); +} + +/** + * e_name_selector_model_peek_section: + * @name_selector_model: an #ENameSelectorModel + * @name: internal name of the section to peek + * @pretty_name: location in which to store a pointer to the user-visible name of the section, + * or %NULL if undesired. + * @destination_store: location in which to store a pointer to the #EDestinationStore being used + * by the section, or %NULL if undesired + * + * Gets the parameters for a destination section. + **/ +gboolean +e_name_selector_model_peek_section (ENameSelectorModel *name_selector_model, + const gchar *name, + gchar **pretty_name, + EDestinationStore **destination_store) +{ + Section *section; + gint n; + + g_return_val_if_fail (E_IS_NAME_SELECTOR_MODEL (name_selector_model), FALSE); + g_return_val_if_fail (name != NULL, FALSE); + + n = find_section_by_name (name_selector_model, name); + if (n < 0) { + g_warning ("ENameSelectorModel does not have a section called '%s'!", name); + return FALSE; + } + + section = &g_array_index (name_selector_model->priv->sections, Section, n); + + if (pretty_name) + *pretty_name = g_strdup (section->pretty_name); + if (destination_store) + *destination_store = section->destination_store; + + return TRUE; +} + +/** + * e_name_selector_model_get_contact_emails_without_used: + * @name_selector_model: an #ENameSelectorModel + * @contact: to get emails from + * @remove_used: set to %TRUE to remove used from a list; or set to %FALSE to + * set used indexes to %NULL and keep them in the returned list + * + * Returns list of all email from @contact, without all used + * in any section. Each item is a string, an email address. + * Returned list should be freed with @e_name_selector_model_free_emails_list. + * + * Since: 2.30 + **/ +GList * +e_name_selector_model_get_contact_emails_without_used (ENameSelectorModel *name_selector_model, + EContact *contact, + gboolean remove_used) +{ + GList *email_list; + gint emails; + gint i; + const gchar *contact_uid; + + g_return_val_if_fail (name_selector_model != NULL, NULL); + g_return_val_if_fail (E_IS_NAME_SELECTOR_MODEL (name_selector_model), NULL); + g_return_val_if_fail (contact != NULL, NULL); + g_return_val_if_fail (E_IS_CONTACT (contact), NULL); + + contact_uid = e_contact_get_const (contact, E_CONTACT_UID); + g_return_val_if_fail (contact_uid != NULL, NULL); + + email_list = e_contact_get (contact, E_CONTACT_EMAIL); + emails = g_list_length (email_list); + + for (i = 0; i < name_selector_model->priv->sections->len; i++) { + Section *section; + GList *destinations; + GList *l; + + section = &g_array_index (name_selector_model->priv->sections, Section, i); + destinations = e_destination_store_list_destinations (section->destination_store); + + for (l = destinations; l; l = g_list_next (l)) { + EDestination *destination = l->data; + const gchar *destination_uid; + + destination_uid = e_destination_get_contact_uid (destination); + if (destination_uid && !strcmp (contact_uid, destination_uid)) { + gint email_num = e_destination_get_email_num (destination); + + if (email_num < 0 || email_num >= emails) { + g_warning ("%s: Destination's email_num %d out of bounds 0..%d", G_STRFUNC, email_num, emails - 1); + } else { + GList *nth = g_list_nth (email_list, email_num); + + g_return_val_if_fail (nth != NULL, NULL); + + g_free (nth->data); + nth->data = NULL; + } + } + } + + g_list_free (destinations); + } + + if (remove_used) { + /* remove all with data NULL, which are those used already */ + do { + emails = g_list_length (email_list); + email_list = g_list_remove (email_list, NULL); + } while (g_list_length (email_list) != emails); + } + + return email_list; +} + +/** + * e_name_selector_model_free_emails_list: + * @email_list: list of emails returned from @e_name_selector_model_get_contact_emails_without_used + * + * Frees a list of emails returned from @e_name_selector_model_get_contact_emails_without_used. + * + * Since: 2.30 + **/ +void +e_name_selector_model_free_emails_list (GList *email_list) +{ + deep_free_list (email_list); +} diff --git a/e-util/e-name-selector-model.h b/e-util/e-name-selector-model.h new file mode 100644 index 0000000000..ed37f2af30 --- /dev/null +++ b/e-util/e-name-selector-model.h @@ -0,0 +1,108 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-name-selector-model.h - Model for contact selection. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_NAME_SELECTOR_MODEL_H +#define E_NAME_SELECTOR_MODEL_H + +#include <e-util/e-tree-model-generator.h> +#include <e-util/e-contact-store.h> +#include <e-util/e-destination-store.h> + +/* Standard GObject macros */ +#define E_TYPE_NAME_SELECTOR_MODEL \ + (e_name_selector_model_get_type ()) +#define E_NAME_SELECTOR_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_NAME_SELECTOR_MODEL, ENameSelectorModel)) +#define E_NAME_SELECTOR_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_NAME_SELECTOR_MODEL, ENameSelectorModelClass)) +#define E_IS_NAME_SELECTOR_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_NAME_SELECTOR_MODEL)) +#define E_IS_NAME_SELECTOR_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_NAME_SELECTOR_MODEL)) +#define E_NAME_SELECTOR_MODEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_NAME_SELECTOR_MODEL, ENameSelectorModelClass)) + +G_BEGIN_DECLS + +typedef struct _ENameSelectorModel ENameSelectorModel; +typedef struct _ENameSelectorModelClass ENameSelectorModelClass; +typedef struct _ENameSelectorModelPrivate ENameSelectorModelPrivate; + +struct _ENameSelectorModel { + GObject parent; + ENameSelectorModelPrivate *priv; +}; + +struct _ENameSelectorModelClass { + GObjectClass parent_class; + + /* Signals */ + void (*section_added) (gchar *name); + void (*section_removed) (gchar *name); +}; + +GType e_name_selector_model_get_type (void); +ENameSelectorModel * + e_name_selector_model_new (void); +EContactStore * e_name_selector_model_peek_contact_store + (ENameSelectorModel *name_selector_model); +ETreeModelGenerator * + e_name_selector_model_peek_contact_filter + (ENameSelectorModel *name_selector_model); + +/* Deep copy of section names; free strings and list when you're done */ +GList * e_name_selector_model_list_sections + (ENameSelectorModel *name_selector_model); + +/* pretty_name will be newly allocated, but destination_store must be reffed if you keep it */ +gboolean e_name_selector_model_peek_section + (ENameSelectorModel *name_selector_model, + const gchar *name, + gchar **pretty_name, + EDestinationStore **destination_store); +void e_name_selector_model_add_section + (ENameSelectorModel *name_selector_model, + const gchar *name, + const gchar *pretty_name, + EDestinationStore *destination_store); +void e_name_selector_model_remove_section + (ENameSelectorModel *name_selector_model, + const gchar *name); +GList * e_name_selector_model_get_contact_emails_without_used + (ENameSelectorModel *name_selector_model, + EContact *contact, + gboolean remove_used); +void e_name_selector_model_free_emails_list + (GList *email_list); + +G_END_DECLS + +#endif /* E_NAME_SELECTOR_MODEL_H */ diff --git a/e-util/e-name-selector.c b/e-util/e-name-selector.c new file mode 100644 index 0000000000..a94b6ff5f3 --- /dev/null +++ b/e-util/e-name-selector.c @@ -0,0 +1,658 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-name-selector.c - Unified context for contact/destination selection UI. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <string.h> +#include <gtk/gtk.h> +#include <glib/gi18n-lib.h> + +#include <libebook/libebook.h> + +#include "e-name-selector.h" + +#include "e-client-utils.h" +#include "e-contact-store.h" +#include "e-destination-store.h" + +#define E_NAME_SELECTOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_NAME_SELECTOR, ENameSelectorPrivate)) + +typedef struct { + gchar *name; + ENameSelectorEntry *entry; +} Section; + +typedef struct { + EBookClient *client; + guint is_completion_book : 1; +} SourceBook; + +struct _ENameSelectorPrivate { + ESourceRegistry *registry; + ENameSelectorModel *model; + ENameSelectorDialog *dialog; + + GArray *sections; + + gboolean books_loaded; + GCancellable *cancellable; + GArray *source_books; +}; + +enum { + PROP_0, + PROP_REGISTRY +}; + +G_DEFINE_TYPE (ENameSelector, e_name_selector, G_TYPE_OBJECT) + +static void +reset_pointer_cb (gpointer data, + GObject *where_was) +{ + ENameSelector *name_selector = data; + ENameSelectorPrivate *priv; + guint ii; + + g_return_if_fail (E_IS_NAME_SELECTOR (name_selector)); + + priv = E_NAME_SELECTOR_GET_PRIVATE (name_selector); + + for (ii = 0; ii < priv->sections->len; ii++) { + Section *section; + + section = &g_array_index (priv->sections, Section, ii); + if (section->entry == (ENameSelectorEntry *) where_was) + section->entry = NULL; + } +} + +static void +name_selector_book_loaded_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + ENameSelector *name_selector = user_data; + ESource *source = E_SOURCE (source_object); + EBookClient *book_client; + EClient *client = NULL; + GArray *sections; + SourceBook source_book; + guint ii; + GError *error = NULL; + + e_client_utils_open_new_finish (source, result, &client, &error); + + if (error != NULL) { + if (!g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE) + && !g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_OFFLINE_UNAVAILABLE) + && !g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_CANCELLED) + && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ( + "ENameSelector: Could not load \"%s\": %s", + e_source_get_display_name (source), error->message); + g_error_free (error); + goto exit; + } + + book_client = E_BOOK_CLIENT (client); + g_return_if_fail (E_IS_BOOK_CLIENT (book_client)); + + source_book.client = book_client; + source_book.is_completion_book = TRUE; + + g_array_append_val (name_selector->priv->source_books, source_book); + + sections = name_selector->priv->sections; + + for (ii = 0; ii < sections->len; ii++) { + EContactStore *store; + Section *section; + + section = &g_array_index (sections, Section, ii); + if (section->entry == NULL) + continue; + + store = e_name_selector_entry_peek_contact_store ( + section->entry); + if (store != NULL) + e_contact_store_add_client (store, book_client); + } + + exit: + g_object_unref (name_selector); +} + +/** + * e_name_selector_load_books: + * @name_selector: an #ENameSelector + * + * Loads address books available for the @name_selector. + * This can be called only once and it can be cancelled + * by e_name_selector_cancel_loading(). + * + * Since: 3.2 + **/ +void +e_name_selector_load_books (ENameSelector *name_selector) +{ + ESourceRegistry *registry; + GList *list, *iter; + const gchar *extension_name; + + g_return_if_fail (E_IS_NAME_SELECTOR (name_selector)); + + extension_name = E_SOURCE_EXTENSION_ADDRESS_BOOK; + registry = e_name_selector_get_registry (name_selector); + list = e_source_registry_list_sources (registry, extension_name); + + for (iter = list; iter != NULL; iter = g_list_next (iter)) { + ESource *source = E_SOURCE (iter->data); + ESourceAutocomplete *extension; + const gchar *extension_name; + + extension_name = E_SOURCE_EXTENSION_AUTOCOMPLETE; + extension = e_source_get_extension (source, extension_name); + + /* Skip disabled address books. */ + if (!e_source_registry_check_enabled (registry, source)) + continue; + + /* Only load address books with autocomplete enabled, + * so as to avoid unnecessary authentication prompts. */ + if (!e_source_autocomplete_get_include_me (extension)) + continue; + + e_client_utils_open_new ( + source, E_CLIENT_SOURCE_TYPE_CONTACTS, + TRUE, name_selector->priv->cancellable, + name_selector_book_loaded_cb, + g_object_ref (name_selector)); + } + + g_list_free_full (list, (GDestroyNotify) g_object_unref); +} + +/** + * e_name_selector_cancel_loading: + * @name_selector: an #ENameSelector + * + * Cancels any pending address book load operations. This might be called + * before an owner unrefs this @name_selector. + * + * Since: 3.2 + **/ +void +e_name_selector_cancel_loading (ENameSelector *name_selector) +{ + g_return_if_fail (E_IS_NAME_SELECTOR (name_selector)); + g_return_if_fail (name_selector->priv->cancellable != NULL); + + g_cancellable_cancel (name_selector->priv->cancellable); +} + +static void +name_selector_set_registry (ENameSelector *name_selector, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (name_selector->priv->registry == NULL); + + name_selector->priv->registry = g_object_ref (registry); +} + +static void +name_selector_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + name_selector_set_registry ( + E_NAME_SELECTOR (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +name_selector_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + g_value_set_object ( + value, + e_name_selector_get_registry ( + E_NAME_SELECTOR (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +name_selector_dispose (GObject *object) +{ + ENameSelectorPrivate *priv; + guint ii; + + priv = E_NAME_SELECTOR_GET_PRIVATE (object); + + if (priv->cancellable) { + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + for (ii = 0; ii < priv->source_books->len; ii++) { + SourceBook *source_book; + + source_book = &g_array_index ( + priv->source_books, SourceBook, ii); + if (source_book->client != NULL) + g_object_unref (source_book->client); + } + + for (ii = 0; ii < priv->sections->len; ii++) { + Section *section; + + section = &g_array_index (priv->sections, Section, ii); + if (section->entry) + g_object_weak_unref ( + G_OBJECT (section->entry), + reset_pointer_cb, object); + g_free (section->name); + } + + g_array_set_size (priv->source_books, 0); + g_array_set_size (priv->sections, 0); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_name_selector_parent_class)->dispose (object); +} + +static void +name_selector_finalize (GObject *object) +{ + ENameSelectorPrivate *priv; + + priv = E_NAME_SELECTOR_GET_PRIVATE (object); + + g_array_free (priv->source_books, TRUE); + g_array_free (priv->sections, TRUE); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_name_selector_parent_class)->finalize (object); +} + +static void +e_name_selector_class_init (ENameSelectorClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ENameSelectorPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = name_selector_set_property; + object_class->get_property = name_selector_get_property; + object_class->dispose = name_selector_dispose; + object_class->finalize = name_selector_finalize; + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + "Data source registry", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_name_selector_init (ENameSelector *name_selector) +{ + GArray *sections; + GArray *source_books; + + sections = g_array_new (FALSE, FALSE, sizeof (Section)); + source_books = g_array_new (FALSE, FALSE, sizeof (SourceBook)); + + name_selector->priv = E_NAME_SELECTOR_GET_PRIVATE (name_selector); + name_selector->priv->sections = sections; + name_selector->priv->model = e_name_selector_model_new (); + name_selector->priv->source_books = source_books; + name_selector->priv->cancellable = g_cancellable_new (); + name_selector->priv->books_loaded = FALSE; +} + +/** + * e_name_selector_new: + * @registry: an #ESourceRegistry + * + * Creates a new #ENameSelector. + * + * Returns: A new #ENameSelector. + **/ +ENameSelector * +e_name_selector_new (ESourceRegistry *registry) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_NAME_SELECTOR, + "registry", registry, NULL); +} + +/** + * e_name_selector_get_registry: + * @name_selector: an #ENameSelector + * + * Returns the #ESourceRegistry passed to e_name_selector_new(). + * + * Returns: the #ESourceRegistry + * + * Since: 3.6 + **/ +ESourceRegistry * +e_name_selector_get_registry (ENameSelector *name_selector) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR (name_selector), NULL); + + return name_selector->priv->registry; +} + +/* ------- * + * Helpers * + * ------- */ + +static gint +add_section (ENameSelector *name_selector, + const gchar *name) +{ + GArray *array; + Section section; + + g_assert (name != NULL); + + memset (§ion, 0, sizeof (Section)); + section.name = g_strdup (name); + + array = name_selector->priv->sections; + g_array_append_val (array, section); + return array->len - 1; +} + +static gint +find_section_by_name (ENameSelector *name_selector, + const gchar *name) +{ + GArray *array; + gint i; + + g_assert (name != NULL); + + array = name_selector->priv->sections; + + for (i = 0; i < array->len; i++) { + Section *section = &g_array_index (array, Section, i); + + if (!strcmp (name, section->name)) + return i; + } + + return -1; +} + +/* ----------------- * + * ENameSelector API * + * ----------------- */ + +/** + * e_name_selector_peek_model: + * @name_selector: an #ENameSelector + * + * Gets the #ENameSelectorModel used by @name_selector. + * + * Returns: The #ENameSelectorModel used by @name_selector. + **/ +ENameSelectorModel * +e_name_selector_peek_model (ENameSelector *name_selector) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR (name_selector), NULL); + + return name_selector->priv->model; +} + +/** + * e_name_selector_peek_dialog: + * @name_selector: an #ENameSelector + * + * Gets the #ENameSelectorDialog used by @name_selector. + * + * Returns: The #ENameSelectorDialog used by @name_selector. + **/ +ENameSelectorDialog * +e_name_selector_peek_dialog (ENameSelector *name_selector) +{ + g_return_val_if_fail (E_IS_NAME_SELECTOR (name_selector), NULL); + + if (name_selector->priv->dialog == NULL) { + ESourceRegistry *registry; + ENameSelectorDialog *dialog; + ENameSelectorModel *model; + + registry = e_name_selector_get_registry (name_selector); + dialog = e_name_selector_dialog_new (registry); + name_selector->priv->dialog = dialog; + + model = e_name_selector_peek_model (name_selector); + e_name_selector_dialog_set_model (dialog, model); + + g_signal_connect ( + dialog, "delete-event", + G_CALLBACK (gtk_widget_hide_on_delete), name_selector); + } + + return name_selector->priv->dialog; +} + +/** + * e_name_selector_show_dialog: + * @name_selector: an #ENameSelector + * @for_transient_widget: a widget parent or %NULL + * + * Shows the associated dialog, and sets the transient parent to the + * GtkWindow top-level of "for_transient_widget if set (it should be) + * + * Since: 2.32 + **/ +void +e_name_selector_show_dialog (ENameSelector *name_selector, + GtkWidget *for_transient_widget) +{ + GtkWindow *top = NULL; + ENameSelectorDialog *dialog; + + g_return_if_fail (E_IS_NAME_SELECTOR (name_selector)); + + dialog = e_name_selector_peek_dialog (name_selector); + if (for_transient_widget) + top = GTK_WINDOW (gtk_widget_get_toplevel (for_transient_widget)); + if (top) + gtk_window_set_transient_for (GTK_WINDOW (dialog), top); + + gtk_widget_show (GTK_WIDGET (dialog)); +} + +/** + * e_name_selector_peek_section_entry: + * @name_selector: an #ENameSelector + * @name: the name of the section to peek + * + * Gets the #ENameSelectorEntry for the section specified by @name. + * + * Returns: The #ENameSelectorEntry for the named section, or %NULL if it + * doesn't exist in the #ENameSelectorModel. + **/ +ENameSelectorEntry * +e_name_selector_peek_section_entry (ENameSelector *name_selector, + const gchar *name) +{ + ENameSelectorPrivate *priv; + ENameSelectorModel *model; + EDestinationStore *destination_store; + Section *section; + gint n; + + g_return_val_if_fail (E_IS_NAME_SELECTOR (name_selector), NULL); + g_return_val_if_fail (name != NULL, NULL); + + priv = E_NAME_SELECTOR_GET_PRIVATE (name_selector); + model = e_name_selector_peek_model (name_selector); + + if (!e_name_selector_model_peek_section ( + model, name, NULL, &destination_store)) + return NULL; + + n = find_section_by_name (name_selector, name); + if (n < 0) + n = add_section (name_selector, name); + + section = &g_array_index (name_selector->priv->sections, Section, n); + + if (!section->entry) { + ESourceRegistry *registry; + EContactStore *contact_store; + gchar *text; + gint i; + + registry = e_name_selector_get_registry (name_selector); + section->entry = e_name_selector_entry_new (registry); + g_object_weak_ref (G_OBJECT (section->entry), reset_pointer_cb, name_selector); + if (pango_parse_markup (name, -1, '_', NULL, + &text, NULL, NULL)) { + atk_object_set_name (gtk_widget_get_accessible (GTK_WIDGET (section->entry)), text); + g_free (text); + } + e_name_selector_entry_set_destination_store (section->entry, destination_store); + + /* Create a contact store for the entry and assign our already-open books to it */ + + contact_store = e_contact_store_new (); + + for (i = 0; i < priv->source_books->len; i++) { + SourceBook *source_book = &g_array_index (priv->source_books, SourceBook, i); + + if (source_book->is_completion_book && source_book->client) + e_contact_store_add_client (contact_store, source_book->client); + } + + e_name_selector_entry_set_contact_store (section->entry, contact_store); + g_object_unref (contact_store); + } + + return section->entry; +} + +/** + * e_name_selector_peek_section_list: + * @name_selector: an #ENameSelector + * @name: the name of the section to peek + * + * Gets the #ENameSelectorList for the section specified by @name. + * + * Returns: The #ENameSelectorList for the named section, or %NULL if it + * doesn't exist in the #ENameSelectorModel. + **/ + +ENameSelectorList * +e_name_selector_peek_section_list (ENameSelector *name_selector, + const gchar *name) +{ + ENameSelectorPrivate *priv; + ENameSelectorModel *model; + EDestinationStore *destination_store; + Section *section; + gint n; + + g_return_val_if_fail (E_IS_NAME_SELECTOR (name_selector), NULL); + g_return_val_if_fail (name != NULL, NULL); + + priv = E_NAME_SELECTOR_GET_PRIVATE (name_selector); + model = e_name_selector_peek_model (name_selector); + + if (!e_name_selector_model_peek_section ( + model, name, NULL, &destination_store)) + return NULL; + + n = find_section_by_name (name_selector, name); + if (n < 0) + n = add_section (name_selector, name); + + section = &g_array_index (name_selector->priv->sections, Section, n); + + if (!section->entry) { + EContactStore *contact_store; + ESourceRegistry *registry; + gchar *text; + gint i; + + registry = name_selector->priv->registry; + section->entry = (ENameSelectorEntry *) + e_name_selector_list_new (registry); + g_object_weak_ref (G_OBJECT (section->entry), reset_pointer_cb, name_selector); + if (pango_parse_markup (name, -1, '_', NULL, + &text, NULL, NULL)) { + atk_object_set_name (gtk_widget_get_accessible (GTK_WIDGET (section->entry)), text); + g_free (text); + } + e_name_selector_entry_set_destination_store (section->entry, destination_store); + + /* Create a contact store for the entry and assign our already-open books to it */ + + contact_store = e_contact_store_new (); + for (i = 0; i < priv->source_books->len; i++) { + SourceBook *source_book = &g_array_index (priv->source_books, SourceBook, i); + + if (source_book->is_completion_book && source_book->client) + e_contact_store_add_client (contact_store, source_book->client); + } + + e_name_selector_entry_set_contact_store (section->entry, contact_store); + g_object_unref (contact_store); + } + + return (ENameSelectorList *) section->entry; +} diff --git a/e-util/e-name-selector.h b/e-util/e-name-selector.h new file mode 100644 index 0000000000..1049699d63 --- /dev/null +++ b/e-util/e-name-selector.h @@ -0,0 +1,94 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-name-selector.h - Unified context for contact/destination selection UI. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_NAME_SELECTOR_H +#define E_NAME_SELECTOR_H + +#include <libedataserver/libedataserver.h> + +#include <e-util/e-name-selector-model.h> +#include <e-util/e-name-selector-dialog.h> +#include <e-util/e-name-selector-entry.h> +#include <e-util/e-name-selector-list.h> + +/* Standard GObject macros */ +#define E_TYPE_NAME_SELECTOR \ + (e_name_selector_get_type ()) +#define E_NAME_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_NAME_SELECTOR, ENameSelector)) +#define E_NAME_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_NAME_SELECTOR, ENameSelectorClass)) +#define E_IS_NAME_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_NAME_SELECTOR)) +#define E_IS_NAME_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_NAME_SELECTOR)) +#define E_NAME_SELECTOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_NAME_SELECTOR, ENameSelectorClass)) + +G_BEGIN_DECLS + +typedef struct _ENameSelector ENameSelector; +typedef struct _ENameSelectorClass ENameSelectorClass; +typedef struct _ENameSelectorPrivate ENameSelectorPrivate; + +struct _ENameSelector { + GObject parent; + ENameSelectorPrivate *priv; +}; + +struct _ENameSelectorClass { + GObjectClass parent_class; +}; + +GType e_name_selector_get_type (void); +ENameSelector * e_name_selector_new (ESourceRegistry *registry); +ESourceRegistry * + e_name_selector_get_registry (ENameSelector *name_selector); +ENameSelectorModel * + e_name_selector_peek_model (ENameSelector *name_selector); +ENameSelectorDialog * + e_name_selector_peek_dialog (ENameSelector *name_selector); +ENameSelectorEntry * + e_name_selector_peek_section_entry + (ENameSelector *name_selector, + const gchar *name); +ENameSelectorList * + e_name_selector_peek_section_list + (ENameSelector *name_selector, + const gchar *name); +void e_name_selector_show_dialog (ENameSelector *name_selector, + GtkWidget *for_transient_widget); +void e_name_selector_load_books (ENameSelector *name_selector); +void e_name_selector_cancel_loading (ENameSelector *name_selector); + +G_END_DECLS + +#endif /* E_NAME_SELECTOR_H */ diff --git a/e-util/e-online-button.c b/e-util/e-online-button.c new file mode 100644 index 0000000000..a3921a779f --- /dev/null +++ b/e-util/e-online-button.c @@ -0,0 +1,210 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-online-button.h" + +#include <glib/gi18n.h> + +#define E_ONLINE_BUTTON_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_ONLINE_BUTTON, EOnlineButtonPrivate)) + +#define ONLINE_TOOLTIP \ + _("Evolution is currently online. Click this button to work offline.") + +#define OFFLINE_TOOLTIP \ + _("Evolution is currently offline. Click this button to work online.") + +#define NETWORK_UNAVAILABLE_TOOLTIP \ + _("Evolution is currently offline because the network is unavailable.") + +struct _EOnlineButtonPrivate { + GtkWidget *image; + gboolean online; +}; + +enum { + PROP_0, + PROP_ONLINE +}; + +G_DEFINE_TYPE ( + EOnlineButton, + e_online_button, + GTK_TYPE_BUTTON) + +static void +online_button_update_tooltip (EOnlineButton *button) +{ + const gchar *tooltip; + + if (e_online_button_get_online (button)) + tooltip = ONLINE_TOOLTIP; + else if (gtk_widget_get_sensitive (GTK_WIDGET (button))) + tooltip = OFFLINE_TOOLTIP; + else + tooltip = NETWORK_UNAVAILABLE_TOOLTIP; + + gtk_widget_set_tooltip_text (GTK_WIDGET (button), tooltip); +} + +static void +online_button_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ONLINE: + e_online_button_set_online ( + E_ONLINE_BUTTON (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +online_button_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ONLINE: + g_value_set_boolean ( + value, e_online_button_get_online ( + E_ONLINE_BUTTON (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +online_button_dispose (GObject *object) +{ + EOnlineButtonPrivate *priv; + + priv = E_ONLINE_BUTTON_GET_PRIVATE (object); + + if (priv->image != NULL) { + g_object_unref (priv->image); + priv->image = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_online_button_parent_class)->dispose (object); +} + +static void +e_online_button_class_init (EOnlineButtonClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EOnlineButtonPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = online_button_set_property; + object_class->get_property = online_button_get_property; + object_class->dispose = online_button_dispose; + + g_object_class_install_property ( + object_class, + PROP_ONLINE, + g_param_spec_boolean ( + "online", + "Online", + "The button state is online", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); +} + +static void +e_online_button_init (EOnlineButton *button) +{ + GtkWidget *widget; + + button->priv = E_ONLINE_BUTTON_GET_PRIVATE (button); + + gtk_widget_set_can_focus (GTK_WIDGET (button), FALSE); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + + widget = gtk_image_new (); + gtk_container_add (GTK_CONTAINER (button), widget); + button->priv->image = g_object_ref (widget); + gtk_widget_show (widget); + + g_signal_connect ( + button, "notify::online", + G_CALLBACK (online_button_update_tooltip), NULL); + + g_signal_connect ( + button, "notify::sensitive", + G_CALLBACK (online_button_update_tooltip), NULL); +} + +GtkWidget * +e_online_button_new (void) +{ + return g_object_new (E_TYPE_ONLINE_BUTTON, NULL); +} + +gboolean +e_online_button_get_online (EOnlineButton *button) +{ + g_return_val_if_fail (E_IS_ONLINE_BUTTON (button), FALSE); + + return button->priv->online; +} + +void +e_online_button_set_online (EOnlineButton *button, + gboolean online) +{ + GtkImage *image; + GtkIconInfo *icon_info; + GtkIconTheme *icon_theme; + const gchar *filename; + const gchar *icon_name; + + g_return_if_fail (E_IS_ONLINE_BUTTON (button)); + + if (button->priv->online == online) + return; + + button->priv->online = online; + + image = GTK_IMAGE (button->priv->image); + icon_name = online ? "online" : "offline"; + icon_theme = gtk_icon_theme_get_default (); + + /* Prevent GTK+ from scaling these rectangular icons. */ + icon_info = gtk_icon_theme_lookup_icon ( + icon_theme, icon_name, GTK_ICON_SIZE_BUTTON, 0); + filename = gtk_icon_info_get_filename (icon_info); + gtk_image_set_from_file (image, filename); + gtk_icon_info_free (icon_info); + + g_object_notify (G_OBJECT (button), "online"); +} diff --git a/e-util/e-online-button.h b/e-util/e-online-button.h new file mode 100644 index 0000000000..073489fef9 --- /dev/null +++ b/e-util/e-online-button.h @@ -0,0 +1,69 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_ONLINE_BUTTON_H +#define E_ONLINE_BUTTON_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_ONLINE_BUTTON \ + (e_online_button_get_type ()) +#define E_ONLINE_BUTTON(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_ONLINE_BUTTON, EOnlineButton)) +#define E_ONLINE_BUTTON_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_ONLINE_BUTTON, EOnlineButtonClass)) +#define E_IS_ONLINE_BUTTON(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_ONLINE_BUTTON)) +#define E_IS_ONLINE_BUTTON_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_ONLINE_BUTTON)) +#define E_ONLINE_BUTTON_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_ONLINE_BUTTON, EOnlineButtonClass)) + +G_BEGIN_DECLS + +typedef struct _EOnlineButton EOnlineButton; +typedef struct _EOnlineButtonClass EOnlineButtonClass; +typedef struct _EOnlineButtonPrivate EOnlineButtonPrivate; + +struct _EOnlineButton { + GtkButton parent; + EOnlineButtonPrivate *priv; +}; + +struct _EOnlineButtonClass { + GtkButtonClass parent_class; +}; + +GType e_online_button_get_type (void); +GtkWidget * e_online_button_new (void); +gboolean e_online_button_get_online (EOnlineButton *button); +void e_online_button_set_online (EOnlineButton *button, + gboolean online); + +G_END_DECLS + +#endif /* E_ONLINE_BUTTON_H */ diff --git a/e-util/e-paned.c b/e-util/e-paned.c new file mode 100644 index 0000000000..3b8f16bbec --- /dev/null +++ b/e-util/e-paned.c @@ -0,0 +1,503 @@ +/* + * e-paned.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-paned.h" + +#include <glib/gi18n-lib.h> + +#define E_PANED_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_PANED, EPanedPrivate)) + +#define SYNC_REQUEST_NONE 0 +#define SYNC_REQUEST_POSITION 1 +#define SYNC_REQUEST_PROPORTION 2 + +struct _EPanedPrivate { + gint hposition; + gint vposition; + gdouble proportion; + + gulong wse_handler_id; + + guint fixed_resize : 1; + guint sync_request : 2; + guint toplevel_ready : 1; +}; + +enum { + PROP_0, + PROP_HPOSITION, + PROP_VPOSITION, + PROP_PROPORTION, + PROP_FIXED_RESIZE +}; + +G_DEFINE_TYPE ( + EPaned, + e_paned, + GTK_TYPE_PANED) + +static gboolean +paned_queue_resize_on_idle (GtkWidget *paned) +{ + gtk_widget_queue_resize_no_redraw (paned); + + return FALSE; +} + +static gboolean +paned_window_state_event_cb (EPaned *paned, + GdkEventWindowState *event, + GtkWidget *toplevel) +{ + /* Wait for WITHDRAWN to change from 1 to 0. */ + if (!(event->changed_mask & GDK_WINDOW_STATE_WITHDRAWN)) + return FALSE; + + /* The whole point of this hack is to trap a point where if + * the window were to be maximized initially, the maximized + * allocation would already be negotiated. We're there now. + * Set a flag so we know it's safe to set GtkPaned position. */ + paned->priv->toplevel_ready = TRUE; + + if (paned->priv->sync_request != SYNC_REQUEST_NONE) + gtk_widget_queue_resize (GTK_WIDGET (paned)); + + /* We don't need to listen for window state events anymore. */ + g_signal_handler_disconnect (toplevel, paned->priv->wse_handler_id); + paned->priv->wse_handler_id = 0; + + return FALSE; +} + +static void +paned_notify_orientation_cb (EPaned *paned) +{ + /* Ignore the next "notify::position" emission. */ + if (e_paned_get_fixed_resize (paned)) + paned->priv->sync_request = SYNC_REQUEST_POSITION; + else + paned->priv->sync_request = SYNC_REQUEST_PROPORTION; + gtk_widget_queue_resize (GTK_WIDGET (paned)); +} + +static void +paned_notify_position_cb (EPaned *paned) +{ + GtkAllocation allocation; + GtkOrientable *orientable; + GtkOrientation orientation; + gdouble proportion; + gint position; + + /* If a sync has already been requested, do nothing. */ + if (paned->priv->sync_request != SYNC_REQUEST_NONE) + return; + + orientable = GTK_ORIENTABLE (paned); + orientation = gtk_orientable_get_orientation (orientable); + + gtk_widget_get_allocation (GTK_WIDGET (paned), &allocation); + position = gtk_paned_get_position (GTK_PANED (paned)); + + g_object_freeze_notify (G_OBJECT (paned)); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + position = MAX (0, allocation.width - position); + proportion = (gdouble) position / allocation.width; + + paned->priv->hposition = position; + g_object_notify (G_OBJECT (paned), "hposition"); + } else { + position = MAX (0, allocation.height - position); + proportion = (gdouble) position / allocation.height; + + paned->priv->vposition = position; + g_object_notify (G_OBJECT (paned), "vposition"); + } + + paned->priv->proportion = proportion; + g_object_notify (G_OBJECT (paned), "proportion"); + + if (e_paned_get_fixed_resize (paned)) + paned->priv->sync_request = SYNC_REQUEST_POSITION; + else + paned->priv->sync_request = SYNC_REQUEST_PROPORTION; + + g_object_thaw_notify (G_OBJECT (paned)); +} + +static void +paned_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_HPOSITION: + e_paned_set_hposition ( + E_PANED (object), + g_value_get_int (value)); + return; + + case PROP_VPOSITION: + e_paned_set_vposition ( + E_PANED (object), + g_value_get_int (value)); + return; + + case PROP_PROPORTION: + e_paned_set_proportion ( + E_PANED (object), + g_value_get_double (value)); + return; + + case PROP_FIXED_RESIZE: + e_paned_set_fixed_resize ( + E_PANED (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +paned_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_HPOSITION: + g_value_set_int ( + value, e_paned_get_hposition ( + E_PANED (object))); + return; + + case PROP_VPOSITION: + g_value_set_int ( + value, e_paned_get_vposition ( + E_PANED (object))); + return; + + case PROP_PROPORTION: + g_value_set_double ( + value, e_paned_get_proportion ( + E_PANED (object))); + return; + + case PROP_FIXED_RESIZE: + g_value_set_boolean ( + value, e_paned_get_fixed_resize ( + E_PANED (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +paned_realize (GtkWidget *widget) +{ + EPanedPrivate *priv; + GtkWidget *toplevel; + GdkWindowState state; + GdkWindow *window; + + priv = E_PANED_GET_PRIVATE (widget); + + /* Chain up to parent's realize() method. */ + GTK_WIDGET_CLASS (e_paned_parent_class)->realize (widget); + + /* XXX This would be easier if we could be notified of + * window state events directly, but I can't seem + * to make that happen. */ + + toplevel = gtk_widget_get_toplevel (widget); + window = gtk_widget_get_window (toplevel); + state = gdk_window_get_state (window); + + /* If the window is withdrawn, wait for it to be shown before + * setting the pane position. If the window is already shown, + * it's safe to set the pane position immediately. */ + if (state & GDK_WINDOW_STATE_WITHDRAWN) + priv->wse_handler_id = g_signal_connect_swapped ( + toplevel, "window-state-event", + G_CALLBACK (paned_window_state_event_cb), widget); + else + priv->toplevel_ready = TRUE; +} + +static void +paned_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + EPaned *paned = E_PANED (widget); + GtkOrientable *orientable; + GtkOrientation orientation; + gdouble proportion; + gint allocated; + gint position; + + /* Chain up to parent's size_allocate() method. */ + GTK_WIDGET_CLASS (e_paned_parent_class)-> + size_allocate (widget, allocation); + + if (!paned->priv->toplevel_ready) + return; + + if (paned->priv->sync_request == SYNC_REQUEST_NONE) + return; + + orientable = GTK_ORIENTABLE (paned); + orientation = gtk_orientable_get_orientation (orientable); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + allocated = allocation->width; + position = e_paned_get_hposition (paned); + } else { + allocated = allocation->height; + position = e_paned_get_vposition (paned); + } + + proportion = e_paned_get_proportion (paned); + + if (paned->priv->sync_request == SYNC_REQUEST_POSITION) + position = MAX (0, allocated - position); + else + position = (1.0 - proportion) * allocated; + + gtk_paned_set_position (GTK_PANED (paned), position); + + paned->priv->sync_request = SYNC_REQUEST_NONE; + + /* gtk_paned_set_position() calls queue_resize, which cannot + * be called from size_allocate, so schedule it from an idle + * callback so the change takes effect. */ + g_idle_add_full ( + G_PRIORITY_DEFAULT_IDLE, + (GSourceFunc) paned_queue_resize_on_idle, + g_object_ref (paned), + (GDestroyNotify) g_object_unref); +} + +static void +e_paned_class_init (EPanedClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EPanedPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = paned_set_property; + object_class->get_property = paned_get_property; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->realize = paned_realize; + widget_class->size_allocate = paned_size_allocate; + + g_object_class_install_property ( + object_class, + PROP_HPOSITION, + g_param_spec_int ( + "hposition", + "Horizontal Position", + "Pane position when oriented horizontally", + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_VPOSITION, + g_param_spec_int ( + "vposition", + "Vertical Position", + "Pane position when oriented vertically", + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_PROPORTION, + g_param_spec_double ( + "proportion", + "Proportion", + "Proportion of the 2nd pane size", + 0.0, + 1.0, + 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_FIXED_RESIZE, + g_param_spec_boolean ( + "fixed-resize", + "Fixed Resize", + "Keep the 2nd pane fixed during resize", + TRUE, + G_PARAM_READWRITE)); +} + +static void +e_paned_init (EPaned *paned) +{ + paned->priv = E_PANED_GET_PRIVATE (paned); + + paned->priv->proportion = 0.5; + paned->priv->fixed_resize = TRUE; + + g_signal_connect ( + paned, "notify::orientation", + G_CALLBACK (paned_notify_orientation_cb), NULL); + + g_signal_connect ( + paned, "notify::position", + G_CALLBACK (paned_notify_position_cb), NULL); +} + +GtkWidget * +e_paned_new (GtkOrientation orientation) +{ + return g_object_new (E_TYPE_PANED, "orientation", orientation, NULL); +} + +gint +e_paned_get_hposition (EPaned *paned) +{ + g_return_val_if_fail (E_IS_PANED (paned), 0); + + return paned->priv->hposition; +} + +void +e_paned_set_hposition (EPaned *paned, + gint hposition) +{ + GtkOrientable *orientable; + GtkOrientation orientation; + + g_return_if_fail (E_IS_PANED (paned)); + + if (hposition == paned->priv->hposition) + return; + + paned->priv->hposition = hposition; + + g_object_notify (G_OBJECT (paned), "hposition"); + + orientable = GTK_ORIENTABLE (paned); + orientation = gtk_orientable_get_orientation (orientable); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + paned->priv->sync_request = SYNC_REQUEST_POSITION; + gtk_widget_queue_resize (GTK_WIDGET (paned)); + } +} + +gint +e_paned_get_vposition (EPaned *paned) +{ + g_return_val_if_fail (E_IS_PANED (paned), 0); + + return paned->priv->vposition; +} + +void +e_paned_set_vposition (EPaned *paned, + gint vposition) +{ + GtkOrientable *orientable; + GtkOrientation orientation; + + g_return_if_fail (E_IS_PANED (paned)); + + if (vposition == paned->priv->vposition) + return; + + paned->priv->vposition = vposition; + + g_object_notify (G_OBJECT (paned), "vposition"); + + orientable = GTK_ORIENTABLE (paned); + orientation = gtk_orientable_get_orientation (orientable); + + if (orientation == GTK_ORIENTATION_VERTICAL) { + paned->priv->sync_request = SYNC_REQUEST_POSITION; + gtk_widget_queue_resize (GTK_WIDGET (paned)); + } +} + +gdouble +e_paned_get_proportion (EPaned *paned) +{ + g_return_val_if_fail (E_IS_PANED (paned), 0.5); + + return paned->priv->proportion; +} + +void +e_paned_set_proportion (EPaned *paned, + gdouble proportion) +{ + g_return_if_fail (E_IS_PANED (paned)); + g_return_if_fail (CLAMP (proportion, 0.0, 1.0) == proportion); + + paned->priv->proportion = proportion; + + paned->priv->sync_request = SYNC_REQUEST_PROPORTION; + gtk_widget_queue_resize (GTK_WIDGET (paned)); + + g_object_notify (G_OBJECT (paned), "proportion"); +} + +gboolean +e_paned_get_fixed_resize (EPaned *paned) +{ + g_return_val_if_fail (E_IS_PANED (paned), FALSE); + + return paned->priv->fixed_resize; +} + +void +e_paned_set_fixed_resize (EPaned *paned, + gboolean fixed_resize) +{ + g_return_if_fail (E_IS_PANED (paned)); + + if (fixed_resize == paned->priv->fixed_resize) + return; + + paned->priv->fixed_resize = fixed_resize; + + g_object_notify (G_OBJECT (paned), "fixed-resize"); +} diff --git a/e-util/e-paned.h b/e-util/e-paned.h new file mode 100644 index 0000000000..79fa3ddfa4 --- /dev/null +++ b/e-util/e-paned.h @@ -0,0 +1,82 @@ +/* + * e-paned.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_PANED_H +#define E_PANED_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_PANED \ + (e_paned_get_type ()) +#define E_PANED(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_PANED, EPaned)) +#define E_PANED_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_PANED, EPanedClass)) +#define E_IS_PANED(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_PANED)) +#define E_IS_PANED_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_PANED)) +#define E_PANED_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_PANED, EPanedClass)) + +G_BEGIN_DECLS + +typedef struct _EPaned EPaned; +typedef struct _EPanedClass EPanedClass; +typedef struct _EPanedPrivate EPanedPrivate; + +struct _EPaned { + GtkPaned parent; + EPanedPrivate *priv; +}; + +struct _EPanedClass { + GtkPanedClass parent_class; +}; + +GType e_paned_get_type (void); +GtkWidget * e_paned_new (GtkOrientation orientation); +gint e_paned_get_hposition (EPaned *paned); +void e_paned_set_hposition (EPaned *paned, + gint hposition); +gint e_paned_get_vposition (EPaned *paned); +void e_paned_set_vposition (EPaned *paned, + gint vposition); +gdouble e_paned_get_proportion (EPaned *paned); +void e_paned_set_proportion (EPaned *paned, + gdouble proportion); +gboolean e_paned_get_fixed_resize (EPaned *paned); +void e_paned_set_fixed_resize (EPaned *paned, + gboolean fixed_resize); + +G_END_DECLS + +#endif /* E_PANED_H */ diff --git a/e-util/e-passwords-win32.c b/e-util/e-passwords-win32.c new file mode 100644 index 0000000000..51c0cb21bb --- /dev/null +++ b/e-util/e-passwords-win32.c @@ -0,0 +1,1064 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* + * e-passwords.c + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA. + */ + +/* + * This looks a lot more complicated than it is, and than you'd think + * it would need to be. There is however, method to the madness. + * + * The code most cope with being called from any thread at any time, + * recursively from the main thread, and then serialising every + * request so that sane and correct values are always returned, and + * duplicate requests are never made. + * + * To this end, every call is marshalled and queued and a dispatch + * method invoked until that request is satisfied. If mainloop + * recursion occurs, then the sub-call will necessarily return out of + * order, but will not be processed out of order. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <limits.h> +#include <stdlib.h> +#include <string.h> +#include <gtk/gtk.h> +#include <glib/gi18n-lib.h> + +#include "e-passwords.h" +#include "libedataserver/e-data-server-util.h" +#include "libedataserver/e-flag.h" +#include "libedataserver/e-url.h" + +#define d(x) + +typedef struct _EPassMsg EPassMsg; + +struct _EPassMsg { + void (*dispatch) (EPassMsg *); + EFlag *done; + + /* input */ + GtkWindow *parent; + const gchar *component; + const gchar *key; + const gchar *title; + const gchar *prompt; + const gchar *oldpass; + guint32 flags; + + /* output */ + gboolean *remember; + gchar *password; + GError *error; + + /* work variables */ + GtkWidget *entry; + GtkWidget *check; + guint ismain:1; + guint noreply:1; /* supress replies; when calling + * dispatch functions from others */ +}; + +G_LOCK_DEFINE_STATIC (passwords); +static GThread *main_thread = NULL; +static GHashTable *password_cache = NULL; +static GtkDialog *password_dialog = NULL; +static GQueue message_queue = G_QUEUE_INIT; +static gint idle_id; +static gint ep_online_state = TRUE; + +#define KEY_FILE_GROUP_PREFIX "Passwords-" +static GKeyFile *key_file = NULL; + +static gboolean +check_key_file (const gchar *funcname) +{ + if (!key_file) + g_message ("%s: Failed to create key file!", funcname); + + return key_file != NULL; +} + +static gchar * +ep_key_file_get_filename (void) +{ + return g_build_filename ( + e_get_user_config_dir (), "credentials", "Passwords", NULL); +} + +static gchar * +ep_key_file_get_group (const gchar *component) +{ + return g_strconcat (KEY_FILE_GROUP_PREFIX, component, NULL); +} + +static gchar * +ep_key_file_normalize_key (const gchar *key) +{ + /* XXX Previous code converted all slashes and equal signs in the + * key to underscores for use with "gnome-config" functions. While + * it may not be necessary to convert slashes for use with GKeyFile, + * we continue to do the same for backward-compatibility. */ + + gchar *normalized_key, *cp; + + normalized_key = g_strdup (key); + for (cp = normalized_key; *cp != '\0'; cp++) + if (*cp == '/' || *cp == '=') + *cp = '_'; + + return normalized_key; +} + +static void +ep_key_file_load (void) +{ + gchar *filename; + GError *error = NULL; + + if (!check_key_file (G_STRFUNC)) + return; + + filename = ep_key_file_get_filename (); + + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) + goto exit; + + g_key_file_load_from_file ( + key_file, filename, G_KEY_FILE_KEEP_COMMENTS | + G_KEY_FILE_KEEP_TRANSLATIONS, &error); + + if (error != NULL) { + g_warning ("%s: %s", filename, error->message); + g_error_free (error); + } + +exit: + g_free (filename); +} + +static void +ep_key_file_save (void) +{ + gchar *contents; + gchar *filename; + gchar *pathname; + gsize length = 0; + GError *error = NULL; + + if (!check_key_file (G_STRFUNC)) + return; + + filename = ep_key_file_get_filename (); + contents = g_key_file_to_data (key_file, &length, &error); + pathname = g_path_get_dirname (filename); + + if (!error) { + g_mkdir_with_parents (pathname, 0700); + g_file_set_contents (filename, contents, length, &error); + } + + g_free (pathname); + + if (error != NULL) { + g_warning ("%s: %s", filename, error->message); + g_error_free (error); + } + + g_free (contents); + g_free (filename); +} + +static gchar * +ep_password_encode (const gchar *password) +{ + /* XXX The previous Base64 encoding function did not encode the + * password's trailing nul byte. This makes decoding the Base64 + * string into a nul-terminated password more difficult, but we + * continue to do it this way for backward-compatibility. */ + + gsize length = strlen (password); + return g_base64_encode ((const guchar *) password, length); +} + +static gchar * +ep_password_decode (const gchar *encoded_password) +{ + /* XXX The previous Base64 encoding function did not encode the + * password's trailing nul byte, so we have to append a nul byte + * to the decoded data to make it a nul-terminated string. */ + + gchar *password; + gsize length = 0; + + password = (gchar *) g_base64_decode (encoded_password, &length); + password = g_realloc (password, length + 1); + password[length] = '\0'; + + return password; +} + +static gboolean +ep_idle_dispatch (gpointer data) +{ + EPassMsg *msg; + + /* As soon as a password window is up we stop; it will + re-invoke us when it has been closed down */ + G_LOCK (passwords); + while (password_dialog == NULL && (msg = g_queue_pop_head (&message_queue)) != NULL) { + G_UNLOCK (passwords); + + msg->dispatch (msg); + + G_LOCK (passwords); + } + + idle_id = 0; + G_UNLOCK (passwords); + + return FALSE; +} + +static EPassMsg * +ep_msg_new (void (*dispatch) (EPassMsg *)) +{ + EPassMsg *msg; + + e_passwords_init (); + + msg = g_malloc0 (sizeof (*msg)); + msg->dispatch = dispatch; + msg->done = e_flag_new (); + msg->ismain = (g_thread_self () == main_thread); + + return msg; +} + +static void +ep_msg_free (EPassMsg *msg) +{ + /* XXX We really should be passing this back to the caller, but + * doing so will require breaking the password API. */ + if (msg->error != NULL) { + g_warning ("%s", msg->error->message); + g_error_free (msg->error); + } + + e_flag_free (msg->done); + g_free (msg->password); + g_free (msg); +} + +static void +ep_msg_send (EPassMsg *msg) +{ + gint needidle = 0; + + G_LOCK (passwords); + g_queue_push_tail (&message_queue, msg); + if (!idle_id) { + if (!msg->ismain) + idle_id = g_idle_add (ep_idle_dispatch, NULL); + else + needidle = 1; + } + G_UNLOCK (passwords); + + if (msg->ismain) { + if (needidle) + ep_idle_dispatch (NULL); + while (!e_flag_is_set (msg->done)) + g_main_context_iteration (NULL, TRUE); + } else + e_flag_wait (msg->done); +} + +/* the functions that actually do the work */ + +static void +ep_clear_passwords_keyfile (EPassMsg *msg) +{ + gchar *group; + GError *error = NULL; + + if (!check_key_file (G_STRFUNC)) + return; + + group = ep_key_file_get_group (msg->component); + + if (g_key_file_remove_group (key_file, group, &error)) + ep_key_file_save (); + + /* Not finding the requested group is acceptable, but we still + * want to leave an informational message on the terminal. */ + else if (g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) { + g_message ("%s", error->message); + g_error_free (error); + + } else if (error != NULL) + g_propagate_error (&msg->error, error); + + g_free (group); +} + +static void +ep_clear_passwords (EPassMsg *msg) +{ + ep_clear_passwords_keyfile (msg); + + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void +ep_forget_passwords_keyfile (EPassMsg *msg) +{ + gchar **groups; + gsize length = 0, ii; + + if (!check_key_file (G_STRFUNC)) + return; + + groups = g_key_file_get_groups (key_file, &length); + + if (!groups) + return; + + for (ii = 0; ii < length; ii++) { + GError *error = NULL; + + if (!g_str_has_prefix (groups[ii], KEY_FILE_GROUP_PREFIX)) + continue; + + g_key_file_remove_group (key_file, groups[ii], &error); + + /* Not finding the requested group is acceptable, but we still + * want to leave an informational message on the terminal. */ + if (g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) { + g_message ("%s", error->message); + g_error_free (error); + + /* Issue a warning if anything else goes wrong. */ + } else if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } + } + ep_key_file_save (); + g_strfreev (groups); +} + +static void +ep_forget_passwords (EPassMsg *msg) +{ + g_hash_table_remove_all (password_cache); + + ep_forget_passwords_keyfile (msg); + + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void +ep_remember_password_keyfile (EPassMsg *msg) +{ + gchar *group, *key, *password; + + password = g_hash_table_lookup (password_cache, msg->key); + if (password == NULL) { + g_warning ("Password for key \"%s\" not found", msg->key); + return; + } + + group = ep_key_file_get_group (msg->component); + key = ep_key_file_normalize_key (msg->key); + password = ep_password_encode (password); + + g_hash_table_remove (password_cache, msg->key); + if (check_key_file (G_STRFUNC)) { + g_key_file_set_string (key_file, group, key, password); + ep_key_file_save (); + } + + g_free (group); + g_free (key); + g_free (password); +} + +static void +ep_remember_password (EPassMsg *msg) +{ + ep_remember_password_keyfile (msg); + + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void +ep_forget_password_keyfile (EPassMsg *msg) +{ + gchar *group, *key; + GError *error = NULL; + + if (!check_key_file (G_STRFUNC)) + return; + + group = ep_key_file_get_group (msg->component); + key = ep_key_file_normalize_key (msg->key); + + if (g_key_file_remove_key (key_file, group, key, &error)) + ep_key_file_save (); + + /* Not finding the requested key is acceptable, but we still + * want to leave an informational message on the terminal. */ + else if (g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { + g_message ("%s", error->message); + g_error_free (error); + + /* Not finding the requested group is also acceptable. */ + } else if (g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) { + g_message ("%s", error->message); + g_error_free (error); + + } else if (error != NULL) + g_propagate_error (&msg->error, error); + + g_free (group); + g_free (key); +} + +static void +ep_forget_password (EPassMsg *msg) +{ + g_hash_table_remove (password_cache, msg->key); + + ep_forget_password_keyfile (msg); + + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void +ep_get_password_keyfile (EPassMsg *msg) +{ + gchar *group, *key, *password; + GError *error = NULL; + + if (!check_key_file (G_STRFUNC)) + return; + + group = ep_key_file_get_group (msg->component); + key = ep_key_file_normalize_key (msg->key); + + password = g_key_file_get_string (key_file, group, key, &error); + if (password != NULL) { + msg->password = ep_password_decode (password); + g_free (password); + + /* Not finding the requested key is acceptable, but we still + * want to leave an informational message on the terminal. */ + } else if (g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { + g_message ("%s", error->message); + g_error_free (error); + + /* Not finding the requested group is also acceptable. */ + } else if (g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) { + g_message ("%s", error->message); + g_error_free (error); + + } else if (error != NULL) + g_propagate_error (&msg->error, error); + + g_free (group); + g_free (key); +} + +static void +ep_get_password (EPassMsg *msg) +{ + gchar *password; + + /* Check the in-memory cache first. */ + password = g_hash_table_lookup (password_cache, msg->key); + if (password != NULL) { + msg->password = g_strdup (password); + + } else + ep_get_password_keyfile (msg); + + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void +ep_add_password (EPassMsg *msg) +{ + g_hash_table_insert ( + password_cache, g_strdup (msg->key), + g_strdup (msg->oldpass)); + + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void ep_ask_password (EPassMsg *msg); + +static void +pass_response (GtkDialog *dialog, + gint response, + gpointer data) +{ + EPassMsg *msg = data; + gint type = msg->flags & E_PASSWORDS_REMEMBER_MASK; + GList *iter, *trash = NULL; + + if (response == GTK_RESPONSE_OK) { + msg->password = g_strdup (gtk_entry_get_text ((GtkEntry *)msg->entry)); + + if (type != E_PASSWORDS_REMEMBER_NEVER) { + gint noreply = msg->noreply; + + *msg->remember = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (msg->check)); + + msg->noreply = 1; + + if (*msg->remember || type == E_PASSWORDS_REMEMBER_FOREVER) { + msg->oldpass = msg->password; + ep_add_password (msg); + } + if (*msg->remember && type == E_PASSWORDS_REMEMBER_FOREVER) + ep_remember_password (msg); + + msg->noreply = noreply; + } + } + + gtk_widget_destroy ((GtkWidget *)dialog); + password_dialog = NULL; + + /* ok, here things get interesting, we suck up any pending + * operations on this specific password, and return the same + * result or ignore other operations */ + + G_LOCK (passwords); + for (iter = g_queue_peek_head_link (&message_queue); iter != NULL; iter = iter->next) { + EPassMsg *pending = iter->data; + + if ((pending->dispatch == ep_forget_password + || pending->dispatch == ep_get_password + || pending->dispatch == ep_ask_password) + && (strcmp (pending->component, msg->component) == 0 + && strcmp (pending->key, msg->key) == 0)) { + + /* Satisfy the pending operation. */ + pending->password = g_strdup (msg->password); + e_flag_set (pending->done); + + /* Mark the queue node for deletion. */ + trash = g_list_prepend (trash, iter); + } + } + + /* Expunge the message queue. */ + for (iter = trash; iter != NULL; iter = iter->next) + g_queue_delete_link (&message_queue, iter->data); + g_list_free (trash); + + G_UNLOCK (passwords); + + if (!msg->noreply) + e_flag_set (msg->done); + + ep_idle_dispatch (NULL); +} + +static gboolean +update_capslock_state (GtkDialog *dialog, + GdkEvent *event, + GtkWidget *label) +{ + GdkModifierType mask = 0; + GdkWindow *window; + gchar *markup = NULL; + GdkDeviceManager *device_manager; + GdkDevice *device; + + device_manager = gdk_display_get_device_manager (gtk_widget_get_display (label)); + device = gdk_device_manager_get_client_pointer (device_manager); + window = gtk_widget_get_window (GTK_WIDGET (dialog)); + gdk_window_get_device_position (window, device, NULL, NULL, &mask); + + /* The space acts as a vertical placeholder. */ + markup = g_markup_printf_escaped ( + "<small>%s</small>", (mask & GDK_LOCK_MASK) ? + _("You have the Caps Lock key on.") : " "); + gtk_label_set_markup (GTK_LABEL (label), markup); + g_free (markup); + + return FALSE; +} + +static void +ep_ask_password (EPassMsg *msg) +{ + GtkWidget *widget; + GtkWidget *container; + GtkWidget *action_area; + GtkWidget *content_area; + gint type = msg->flags & E_PASSWORDS_REMEMBER_MASK; + guint noreply = msg->noreply; + gboolean visible; + AtkObject *a11y; + + msg->noreply = 1; + + widget = gtk_dialog_new_with_buttons ( + msg->title, msg->parent, 0, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, + NULL); +#if !GTK_CHECK_VERSION(2,90,7) + g_object_set (widget, "has-separator", FALSE, NULL); +#endif + gtk_dialog_set_default_response ( + GTK_DIALOG (widget), GTK_RESPONSE_OK); + gtk_window_set_resizable (GTK_WINDOW (widget), FALSE); + gtk_window_set_transient_for (GTK_WINDOW (widget), msg->parent); + gtk_window_set_position (GTK_WINDOW (widget), GTK_WIN_POS_CENTER_ON_PARENT); + gtk_container_set_border_width (GTK_CONTAINER (widget), 12); + password_dialog = GTK_DIALOG (widget); + + action_area = gtk_dialog_get_action_area (password_dialog); + content_area = gtk_dialog_get_content_area (password_dialog); + + /* Override GtkDialog defaults */ + gtk_box_set_spacing (GTK_BOX (action_area), 12); + gtk_container_set_border_width (GTK_CONTAINER (action_area), 0); + gtk_box_set_spacing (GTK_BOX (content_area), 12); + gtk_container_set_border_width (GTK_CONTAINER (content_area), 0); + + /* Grid */ + container = gtk_grid_new (); + gtk_grid_set_column_spacing (GTK_GRID (container), 12); + gtk_grid_set_row_spacing (GTK_GRID (container), 6); + gtk_widget_show (container); + + gtk_box_pack_start ( + GTK_BOX (content_area), container, FALSE, TRUE, 0); + + /* Password Image */ + widget = gtk_image_new_from_icon_name ( + "dialog-password", GTK_ICON_SIZE_DIALOG); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.0); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + + gtk_grid_attach (GTK_GRID (container), widget, 0, 0, 1, 3); + + /* Password Label */ + widget = gtk_label_new (NULL); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_label_set_markup (GTK_LABEL (widget), msg->prompt); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + g_object_set (G_OBJECT (widget), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + + gtk_grid_attach (GTK_GRID (container), widget, 1, 0, 1, 1); + + /* Password Entry */ + widget = gtk_entry_new (); + a11y = gtk_widget_get_accessible (widget); + visible = !(msg->flags & E_PASSWORDS_SECRET); + atk_object_set_description (a11y, msg->prompt); + gtk_entry_set_visibility (GTK_ENTRY (widget), visible); + gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE); + gtk_widget_grab_focus (widget); + g_object_set (G_OBJECT (widget), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + msg->entry = widget; + + if ((msg->flags & E_PASSWORDS_REPROMPT)) { + ep_get_password (msg); + if (msg->password != NULL) { + gtk_entry_set_text (GTK_ENTRY (widget), msg->password); + g_free (msg->password); + msg->password = NULL; + } + } + + gtk_grid_attach (GTK_GRID (container), widget, 1, 1, 1, 1); + + /* Caps Lock Label */ + widget = gtk_label_new (NULL); + g_object_set (G_OBJECT (widget), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + + gtk_grid_attach (GTK_GRID (container), widget, 1, 2, 1, 1); + + g_signal_connect ( + password_dialog, "key-release-event", + G_CALLBACK (update_capslock_state), widget); + g_signal_connect ( + password_dialog, "focus-in-event", + G_CALLBACK (update_capslock_state), widget); + + /* static password, shouldn't be remembered between sessions, + but will be remembered within the session beyond our control */ + if (type != E_PASSWORDS_REMEMBER_NEVER) { + if (msg->flags & E_PASSWORDS_PASSPHRASE) { + widget = gtk_check_button_new_with_mnemonic ( + (type == E_PASSWORDS_REMEMBER_FOREVER) + ? _("_Remember this passphrase") + : _("_Remember this passphrase for" + " the remainder of this session")); + } else { + widget = gtk_check_button_new_with_mnemonic ( + (type == E_PASSWORDS_REMEMBER_FOREVER) + ? _("_Remember this password") + : _("_Remember this password for" + " the remainder of this session")); + } + + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (widget), *msg->remember); + if (msg->flags & E_PASSWORDS_DISABLE_REMEMBER) + gtk_widget_set_sensitive (widget, FALSE); + g_object_set (G_OBJECT (widget), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + msg->check = widget; + + gtk_grid_attach (GTK_GRID (container), widget, 1, 3, 1, 1); + } + + msg->noreply = noreply; + + g_signal_connect ( + password_dialog, "response", + G_CALLBACK (pass_response), msg); + + if (msg->parent) { + gtk_dialog_run (GTK_DIALOG (password_dialog)); + } else { + gtk_window_present (GTK_WINDOW (password_dialog)); + /* workaround GTK+ bug (see Gnome's bugzilla bug #624229) */ + gtk_grab_add (GTK_WIDGET (password_dialog)); + } +} + +/** + * e_passwords_init: + * + * Initializes the e_passwords routines. Must be called before any other + * e_passwords_* function. + **/ +void +e_passwords_init (void) +{ + G_LOCK (passwords); + + if (password_cache == NULL) { + password_cache = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + main_thread = g_thread_self (); + + /* Load the keyfile even if we're using the keyring. + * We might be able to extract passwords from it. */ + key_file = g_key_file_new (); + ep_key_file_load (); + + } + + G_UNLOCK (passwords); +} + +/** + * e_passwords_cancel: + * + * Cancel any outstanding password operations and close any dialogues + * currently being shown. + **/ +void +e_passwords_cancel (void) +{ + EPassMsg *msg; + + G_LOCK (passwords); + while ((msg = g_queue_pop_head (&message_queue)) != NULL) + e_flag_set (msg->done); + G_UNLOCK (passwords); + + if (password_dialog) + gtk_dialog_response (password_dialog, GTK_RESPONSE_CANCEL); +} + +/** + * e_passwords_shutdown: + * + * Cleanup routine to call before exiting. + **/ +void +e_passwords_shutdown (void) +{ + EPassMsg *msg; + + G_LOCK (passwords); + + while ((msg = g_queue_pop_head (&message_queue)) != NULL) + e_flag_set (msg->done); + + if (password_cache != NULL) { + g_hash_table_destroy (password_cache); + password_cache = NULL; + } + + + G_UNLOCK (passwords); + + if (password_dialog != NULL) + gtk_dialog_response (password_dialog, GTK_RESPONSE_CANCEL); +} + +/** + * e_passwords_set_online: + * @state: + * + * Set the offline-state of the application. This is a work-around + * for having the backends fully offline aware, and returns a + * cancellation response instead of prompting for passwords. + * + * FIXME: This is not a permanent api, review post 2.0. + **/ +void +e_passwords_set_online (gint state) +{ + ep_online_state = state; + /* TODO: we could check that a request is open and close it, or maybe who cares */ +} + +/** + * e_passwords_forget_passwords: + * + * Forgets all cached passwords, in memory and on disk. + **/ +void +e_passwords_forget_passwords (void) +{ + EPassMsg *msg = ep_msg_new (ep_forget_passwords); + + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_clear_passwords: + * + * Forgets all disk cached passwords for the component. + **/ +void +e_passwords_clear_passwords (const gchar *component_name) +{ + EPassMsg *msg = ep_msg_new (ep_clear_passwords); + + msg->component = component_name; + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_remember_password: + * @key: the key + * + * Saves the password associated with @key to disk. + **/ +void +e_passwords_remember_password (const gchar *component_name, + const gchar *key) +{ + EPassMsg *msg; + + g_return_if_fail (component_name != NULL); + g_return_if_fail (key != NULL); + + msg = ep_msg_new (ep_remember_password); + msg->component = component_name; + msg->key = key; + + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_forget_password: + * @key: the key + * + * Forgets the password associated with @key, in memory and on disk. + **/ +void +e_passwords_forget_password (const gchar *component_name, + const gchar *key) +{ + EPassMsg *msg; + + g_return_if_fail (component_name != NULL); + g_return_if_fail (key != NULL); + + msg = ep_msg_new (ep_forget_password); + msg->component = component_name; + msg->key = key; + + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_get_password: + * @key: the key + * + * Returns: the password associated with @key, or %NULL. Caller + * must free the returned password. + **/ +gchar * +e_passwords_get_password (const gchar *component_name, + const gchar *key) +{ + EPassMsg *msg; + gchar *passwd; + + g_return_val_if_fail (component_name != NULL, NULL); + g_return_val_if_fail (key != NULL, NULL); + + msg = ep_msg_new (ep_get_password); + msg->component = component_name; + msg->key = key; + + ep_msg_send (msg); + + passwd = msg->password; + msg->password = NULL; + ep_msg_free (msg); + + return passwd; +} + +/** + * e_passwords_add_password: + * @key: a key + * @passwd: the password for @key + * + * This stores the @key/@passwd pair in the current session's password + * hash. + **/ +void +e_passwords_add_password (const gchar *key, + const gchar *passwd) +{ + EPassMsg *msg; + + g_return_if_fail (key != NULL); + g_return_if_fail (passwd != NULL); + + msg = ep_msg_new (ep_add_password); + msg->key = key; + msg->oldpass = passwd; + + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_ask_password: + * @title: title for the password dialog + * @component_name: the name of the component for which we're storing + * the password (e.g. Mail, Addressbook, etc.) + * @key: key to store the password under + * @prompt: prompt string + * @type: whether or not to offer to remember the password, + * and for how long. + * @remember: on input, the default state of the remember checkbox. + * on output, the state of the checkbox when the dialog was closed. + * @parent: parent window of the dialog, or %NULL + * + * Asks the user for a password. + * + * Returns: the password, which the caller must free, or %NULL if + * the user cancelled the operation. *@remember will be set if the + * return value is non-%NULL and @remember_type is not + * E_PASSWORDS_DO_NOT_REMEMBER. + **/ +gchar * +e_passwords_ask_password (const gchar *title, + const gchar *component_name, + const gchar *key, + const gchar *prompt, + EPasswordsRememberType type, + gboolean *remember, + GtkWindow *parent) +{ + gchar *passwd; + EPassMsg *msg; + + g_return_val_if_fail (component_name != NULL, NULL); + g_return_val_if_fail (key != NULL, NULL); + + if ((type & E_PASSWORDS_ONLINE) && !ep_online_state) + return NULL; + + msg = ep_msg_new (ep_ask_password); + msg->title = title; + msg->component = component_name; + msg->key = key; + msg->prompt = prompt; + msg->flags = type; + msg->remember = remember; + msg->parent = parent; + + ep_msg_send (msg); + passwd = msg->password; + msg->password = NULL; + ep_msg_free (msg); + + return passwd; +} diff --git a/e-util/e-passwords.c b/e-util/e-passwords.c new file mode 100644 index 0000000000..bf4cfc1e7f --- /dev/null +++ b/e-util/e-passwords.c @@ -0,0 +1,890 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* + * e-passwords.c + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA. + */ + +/* + * This looks a lot more complicated than it is, and than you'd think + * it would need to be. There is however, method to the madness. + * + * The code must cope with being called from any thread at any time, + * recursively from the main thread, and then serialising every + * request so that sane and correct values are always returned, and + * duplicate requests are never made. + * + * To this end, every call is marshalled and queued and a dispatch + * method invoked until that request is satisfied. If mainloop + * recursion occurs, then the sub-call will necessarily return out of + * order, but will not be processed out of order. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <limits.h> +#include <stdlib.h> +#include <string.h> +#include <gtk/gtk.h> +#include <glib/gi18n-lib.h> + +/* XXX Yeah, yeah... */ +#define SECRET_API_SUBJECT_TO_CHANGE + +#include <libsecret/secret.h> + +#include <libedataserver/libedataserver.h> + +#include "e-passwords.h" + +#define d(x) + +typedef struct _EPassMsg EPassMsg; + +struct _EPassMsg { + void (*dispatch) (EPassMsg *); + EFlag *done; + + /* input */ + GtkWindow *parent; + const gchar *key; + const gchar *title; + const gchar *prompt; + const gchar *oldpass; + guint32 flags; + + /* output */ + gboolean *remember; + gchar *password; + GError *error; + + /* work variables */ + GtkWidget *entry; + GtkWidget *check; + guint ismain : 1; + guint noreply:1; /* supress replies; when calling + * dispatch functions from others */ +}; + +/* XXX probably want to share this with evalution-source-registry-migrate-sources.c */ +static const SecretSchema e_passwords_schema = { + "org.gnome.Evolution.Password", + SECRET_SCHEMA_DONT_MATCH_NAME, + { + { "application", SECRET_SCHEMA_ATTRIBUTE_STRING, }, + { "user", SECRET_SCHEMA_ATTRIBUTE_STRING, }, + { "server", SECRET_SCHEMA_ATTRIBUTE_STRING, }, + { "protocol", SECRET_SCHEMA_ATTRIBUTE_STRING, }, + } +}; + +G_LOCK_DEFINE_STATIC (passwords); +static GThread *main_thread = NULL; +static GHashTable *password_cache = NULL; +static GtkDialog *password_dialog = NULL; +static GQueue message_queue = G_QUEUE_INIT; +static gint idle_id; +static gint ep_online_state = TRUE; + +static EUri * +ep_keyring_uri_new (const gchar *string, + GError **error) +{ + EUri *uri; + + uri = e_uri_new (string); + g_return_val_if_fail (uri != NULL, NULL); + + /* LDAP URIs do not have usernames, so use the URI as the username. */ + if (uri->user == NULL && uri->protocol != NULL && + (strcmp (uri->protocol, "ldap") == 0|| strcmp (uri->protocol, "google") == 0)) + uri->user = g_strdelimit (g_strdup (string), "/=", '_'); + + /* Make sure the URI has the required components. */ + if (uri->user == NULL && uri->host == NULL) { + g_set_error_literal ( + error, G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + _("Keyring key is unusable: no user or host name")); + e_uri_free (uri); + uri = NULL; + } + + return uri; +} + +static gboolean +ep_idle_dispatch (gpointer data) +{ + EPassMsg *msg; + + /* As soon as a password window is up we stop; it will + * re - invoke us when it has been closed down */ + G_LOCK (passwords); + while (password_dialog == NULL && (msg = g_queue_pop_head (&message_queue)) != NULL) { + G_UNLOCK (passwords); + + msg->dispatch (msg); + + G_LOCK (passwords); + } + + idle_id = 0; + G_UNLOCK (passwords); + + return FALSE; +} + +static EPassMsg * +ep_msg_new (void (*dispatch) (EPassMsg *)) +{ + EPassMsg *msg; + + e_passwords_init (); + + msg = g_malloc0 (sizeof (*msg)); + msg->dispatch = dispatch; + msg->done = e_flag_new (); + msg->ismain = (g_thread_self () == main_thread); + + return msg; +} + +static void +ep_msg_free (EPassMsg *msg) +{ + /* XXX We really should be passing this back to the caller, but + * doing so will require breaking the password API. */ + if (msg->error != NULL) { + g_warning ("%s", msg->error->message); + g_error_free (msg->error); + } + + e_flag_free (msg->done); + g_free (msg->password); + g_free (msg); +} + +static void +ep_msg_send (EPassMsg *msg) +{ + gint needidle = 0; + + G_LOCK (passwords); + g_queue_push_tail (&message_queue, msg); + if (!idle_id) { + if (!msg->ismain) + idle_id = g_idle_add (ep_idle_dispatch, NULL); + else + needidle = 1; + } + G_UNLOCK (passwords); + + if (msg->ismain) { + if (needidle) + ep_idle_dispatch (NULL); + while (!e_flag_is_set (msg->done)) + g_main_context_iteration (NULL, TRUE); + } else + e_flag_wait (msg->done); +} + +/* the functions that actually do the work */ + +static void +ep_clear_passwords (EPassMsg *msg) +{ + GError *error = NULL; + + /* Find all Evolution passwords and delete them. */ + secret_password_clear_sync ( + &e_passwords_schema, NULL, &error, + "application", "Evolution", NULL); + + if (error != NULL) + g_propagate_error (&msg->error, error); + + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void +ep_remember_password (EPassMsg *msg) +{ + gchar *password; + EUri *uri; + GError *error = NULL; + + password = g_hash_table_lookup (password_cache, msg->key); + if (password == NULL) { + g_warning ("Password for key \"%s\" not found", msg->key); + goto exit; + } + + uri = ep_keyring_uri_new (msg->key, &msg->error); + if (uri == NULL) + goto exit; + + secret_password_store_sync ( + &e_passwords_schema, + SECRET_COLLECTION_DEFAULT, + msg->key, password, + NULL, &error, + "application", "Evolution", + "user", uri->user, + "server", uri->host, + "protocol", uri->protocol, + NULL); + + /* Only remove the password from the session hash + * if the keyring insertion was successful. */ + if (error == NULL) + g_hash_table_remove (password_cache, msg->key); + else + g_propagate_error (&msg->error, error); + + e_uri_free (uri); + +exit: + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void +ep_forget_password (EPassMsg *msg) +{ + EUri *uri; + GError *error = NULL; + + g_hash_table_remove (password_cache, msg->key); + + uri = ep_keyring_uri_new (msg->key, &msg->error); + if (uri == NULL) + goto exit; + + /* Find all Evolution passwords matching the URI and delete them. + * + * XXX We didn't always store protocols in the keyring, so for + * backward-compatibility we need to lookup passwords by user + * and host only (no protocol). But we do send the protocol + * to ep_keyring_delete_passwords(), which also knows about + * the backward-compatibility issue and will filter the list + * appropriately. */ + secret_password_clear_sync ( + &e_passwords_schema, NULL, &error, + "application", "Evolution", + "user", uri->user, + "server", uri->host, + NULL); + + if (error != NULL) + g_propagate_error (&msg->error, error); + + e_uri_free (uri); + +exit: + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void +ep_get_password (EPassMsg *msg) +{ + EUri *uri; + gchar *password; + GError *error = NULL; + + /* Check the in-memory cache first. */ + password = g_hash_table_lookup (password_cache, msg->key); + if (password != NULL) { + msg->password = g_strdup (password); + goto exit; + } + + uri = ep_keyring_uri_new (msg->key, &msg->error); + if (uri == NULL) + goto exit; + + msg->password = secret_password_lookup_sync ( + &e_passwords_schema, NULL, &error, + "application", "Evolution", + "user", uri->user, + "server", uri->host, + "protocol", uri->protocol, + NULL); + + if (msg->password != NULL) + goto done; + + /* Clear the previous error, if there was one. + * It's likely to occur again. */ + if (error != NULL) + g_clear_error (&error); + + /* XXX We didn't always store protocols in the keyring, so for + * backward-compatibility we also need to lookup passwords + * by user and host only (no protocol). */ + msg->password = secret_password_lookup_sync ( + &e_passwords_schema, NULL, &error, + "application", "Evolution", + "user", uri->user, + "server", uri->host, + NULL); + +done: + if (error != NULL) + g_propagate_error (&msg->error, error); + + e_uri_free (uri); + +exit: + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void +ep_add_password (EPassMsg *msg) +{ + g_hash_table_insert ( + password_cache, g_strdup (msg->key), + g_strdup (msg->oldpass)); + + if (!msg->noreply) + e_flag_set (msg->done); +} + +static void ep_ask_password (EPassMsg *msg); + +static void +pass_response (GtkDialog *dialog, + gint response, + gpointer data) +{ + EPassMsg *msg = data; + gint type = msg->flags & E_PASSWORDS_REMEMBER_MASK; + GList *iter, *trash = NULL; + + if (response == GTK_RESPONSE_OK) { + msg->password = g_strdup (gtk_entry_get_text ((GtkEntry *) msg->entry)); + + if (type != E_PASSWORDS_REMEMBER_NEVER) { + gint noreply = msg->noreply; + + *msg->remember = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (msg->check)); + + msg->noreply = 1; + + if (*msg->remember || type == E_PASSWORDS_REMEMBER_FOREVER) { + msg->oldpass = msg->password; + ep_add_password (msg); + } + if (*msg->remember && type == E_PASSWORDS_REMEMBER_FOREVER) + ep_remember_password (msg); + + msg->noreply = noreply; + } + } + + gtk_widget_destroy ((GtkWidget *) dialog); + password_dialog = NULL; + + /* ok, here things get interesting, we suck up any pending + * operations on this specific password, and return the same + * result or ignore other operations */ + + G_LOCK (passwords); + for (iter = g_queue_peek_head_link (&message_queue); iter != NULL; iter = iter->next) { + EPassMsg *pending = iter->data; + + if ((pending->dispatch == ep_forget_password + || pending->dispatch == ep_get_password + || pending->dispatch == ep_ask_password) + && strcmp (pending->key, msg->key) == 0) { + + /* Satisfy the pending operation. */ + pending->password = g_strdup (msg->password); + e_flag_set (pending->done); + + /* Mark the queue node for deletion. */ + trash = g_list_prepend (trash, iter); + } + } + + /* Expunge the message queue. */ + for (iter = trash; iter != NULL; iter = iter->next) + g_queue_delete_link (&message_queue, iter->data); + g_list_free (trash); + + G_UNLOCK (passwords); + + if (!msg->noreply) + e_flag_set (msg->done); + + ep_idle_dispatch (NULL); +} + +static gboolean +update_capslock_state (GtkDialog *dialog, + GdkEvent *event, + GtkWidget *label) +{ + GdkModifierType mask = 0; + GdkWindow *window; + gchar *markup = NULL; + GdkDeviceManager *device_manager; + GdkDevice *device; + + device_manager = gdk_display_get_device_manager (gtk_widget_get_display (label)); + device = gdk_device_manager_get_client_pointer (device_manager); + window = gtk_widget_get_window (GTK_WIDGET (dialog)); + gdk_window_get_device_position (window, device, NULL, NULL, &mask); + + /* The space acts as a vertical placeholder. */ + markup = g_markup_printf_escaped ( + "<small>%s</small>", (mask & GDK_LOCK_MASK) ? + _("You have the Caps Lock key on.") : " "); + gtk_label_set_markup (GTK_LABEL (label), markup); + g_free (markup); + + return FALSE; +} + +static void +ep_ask_password (EPassMsg *msg) +{ + GtkWidget *widget; + GtkWidget *container; + GtkWidget *action_area; + GtkWidget *content_area; + gint type = msg->flags & E_PASSWORDS_REMEMBER_MASK; + guint noreply = msg->noreply; + gboolean visible; + AtkObject *a11y; + + msg->noreply = 1; + + widget = gtk_dialog_new_with_buttons ( + msg->title, msg->parent, 0, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, + NULL); + gtk_dialog_set_default_response ( + GTK_DIALOG (widget), GTK_RESPONSE_OK); + gtk_window_set_resizable (GTK_WINDOW (widget), FALSE); + gtk_window_set_transient_for (GTK_WINDOW (widget), msg->parent); + gtk_window_set_position (GTK_WINDOW (widget), GTK_WIN_POS_CENTER_ON_PARENT); + gtk_container_set_border_width (GTK_CONTAINER (widget), 12); + password_dialog = GTK_DIALOG (widget); + + action_area = gtk_dialog_get_action_area (password_dialog); + content_area = gtk_dialog_get_content_area (password_dialog); + + /* Override GtkDialog defaults */ + gtk_box_set_spacing (GTK_BOX (action_area), 12); + gtk_container_set_border_width (GTK_CONTAINER (action_area), 0); + gtk_box_set_spacing (GTK_BOX (content_area), 12); + gtk_container_set_border_width (GTK_CONTAINER (content_area), 0); + + /* Grid */ + container = gtk_grid_new (); + gtk_grid_set_column_spacing (GTK_GRID (container), 12); + gtk_grid_set_row_spacing (GTK_GRID (container), 6); + gtk_widget_show (container); + + gtk_box_pack_start ( + GTK_BOX (content_area), container, FALSE, TRUE, 0); + + /* Password Image */ + widget = gtk_image_new_from_icon_name ( + "dialog-password", GTK_ICON_SIZE_DIALOG); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.0); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + + gtk_grid_attach (GTK_GRID (container), widget, 0, 0, 1, 3); + + /* Password Label */ + widget = gtk_label_new (NULL); + gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); + gtk_label_set_markup (GTK_LABEL (widget), msg->prompt); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + g_object_set (G_OBJECT (widget), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + + gtk_grid_attach (GTK_GRID (container), widget, 1, 0, 1, 1); + + /* Password Entry */ + widget = gtk_entry_new (); + a11y = gtk_widget_get_accessible (widget); + visible = !(msg->flags & E_PASSWORDS_SECRET); + atk_object_set_description (a11y, msg->prompt); + gtk_entry_set_visibility (GTK_ENTRY (widget), visible); + gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE); + gtk_widget_grab_focus (widget); + g_object_set (G_OBJECT (widget), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + msg->entry = widget; + + if ((msg->flags & E_PASSWORDS_REPROMPT)) { + ep_get_password (msg); + if (msg->password != NULL) { + gtk_entry_set_text (GTK_ENTRY (widget), msg->password); + g_free (msg->password); + msg->password = NULL; + } + } + + gtk_grid_attach (GTK_GRID (container), widget, 1, 1, 1, 1); + + /* Caps Lock Label */ + widget = gtk_label_new (NULL); + g_object_set (G_OBJECT (widget), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + + gtk_grid_attach (GTK_GRID (container), widget, 1, 2, 1, 1); + + g_signal_connect ( + password_dialog, "key-release-event", + G_CALLBACK (update_capslock_state), widget); + g_signal_connect ( + password_dialog, "focus-in-event", + G_CALLBACK (update_capslock_state), widget); + + /* static password, shouldn't be remembered between sessions, + * but will be remembered within the session beyond our control */ + if (type != E_PASSWORDS_REMEMBER_NEVER) { + if (msg->flags & E_PASSWORDS_PASSPHRASE) { + widget = gtk_check_button_new_with_mnemonic ( + (type == E_PASSWORDS_REMEMBER_FOREVER) + ? _("_Remember this passphrase") + : _("_Remember this passphrase for" + " the remainder of this session")); + } else { + widget = gtk_check_button_new_with_mnemonic ( + (type == E_PASSWORDS_REMEMBER_FOREVER) + ? _("_Remember this password") + : _("_Remember this password for" + " the remainder of this session")); + } + + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (widget), *msg->remember); + if (msg->flags & E_PASSWORDS_DISABLE_REMEMBER) + gtk_widget_set_sensitive (widget, FALSE); + g_object_set (G_OBJECT (widget), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_widget_show (widget); + msg->check = widget; + + gtk_grid_attach (GTK_GRID (container), widget, 1, 3, 1, 1); + } + + msg->noreply = noreply; + + g_signal_connect ( + password_dialog, "response", + G_CALLBACK (pass_response), msg); + + if (msg->parent) { + gtk_dialog_run (GTK_DIALOG (password_dialog)); + } else { + gtk_window_present (GTK_WINDOW (password_dialog)); + /* workaround GTK+ bug (see Gnome's bugzilla bug #624229) */ + gtk_grab_add (GTK_WIDGET (password_dialog)); + } +} + +/** + * e_passwords_init: + * + * Initializes the e_passwords routines. Must be called before any other + * e_passwords_* function. + **/ +void +e_passwords_init (void) +{ + G_LOCK (passwords); + + if (password_cache == NULL) { + password_cache = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + main_thread = g_thread_self (); + } + + G_UNLOCK (passwords); +} + +/** + * e_passwords_cancel: + * + * Cancel any outstanding password operations and close any dialogues + * currently being shown. + **/ +void +e_passwords_cancel (void) +{ + EPassMsg *msg; + + G_LOCK (passwords); + while ((msg = g_queue_pop_head (&message_queue)) != NULL) + e_flag_set (msg->done); + G_UNLOCK (passwords); + + if (password_dialog) + gtk_dialog_response (password_dialog, GTK_RESPONSE_CANCEL); +} + +/** + * e_passwords_shutdown: + * + * Cleanup routine to call before exiting. + **/ +void +e_passwords_shutdown (void) +{ + EPassMsg *msg; + + G_LOCK (passwords); + + while ((msg = g_queue_pop_head (&message_queue)) != NULL) + e_flag_set (msg->done); + + if (password_cache != NULL) { + g_hash_table_destroy (password_cache); + password_cache = NULL; + } + + G_UNLOCK (passwords); + + if (password_dialog != NULL) + gtk_dialog_response (password_dialog, GTK_RESPONSE_CANCEL); +} + +/** + * e_passwords_set_online: + * @state: + * + * Set the offline-state of the application. This is a work-around + * for having the backends fully offline aware, and returns a + * cancellation response instead of prompting for passwords. + * + * FIXME: This is not a permanent api, review post 2.0. + **/ +void +e_passwords_set_online (gint state) +{ + ep_online_state = state; + /* TODO: we could check that a request is open and close it, or maybe who cares */ +} + +/** + * e_passwords_forget_passwords: + * + * Forgets all cached passwords, in memory and on disk. + **/ +void +e_passwords_forget_passwords (void) +{ + EPassMsg *msg = ep_msg_new (ep_clear_passwords); + + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_clear_passwords: + * + * Forgets all disk cached passwords for the component. + **/ +void +e_passwords_clear_passwords (const gchar *unused) +{ + EPassMsg *msg = ep_msg_new (ep_clear_passwords); + + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_remember_password: + * @key: the key + * + * Saves the password associated with @key to disk. + **/ +void +e_passwords_remember_password (const gchar *unused, + const gchar *key) +{ + EPassMsg *msg; + + g_return_if_fail (key != NULL); + + msg = ep_msg_new (ep_remember_password); + msg->key = key; + + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_forget_password: + * @key: the key + * + * Forgets the password associated with @key, in memory and on disk. + **/ +void +e_passwords_forget_password (const gchar *unused, + const gchar *key) +{ + EPassMsg *msg; + + g_return_if_fail (key != NULL); + + msg = ep_msg_new (ep_forget_password); + msg->key = key; + + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_get_password: + * @key: the key + * + * Returns: the password associated with @key, or %NULL. Caller + * must free the returned password. + **/ +gchar * +e_passwords_get_password (const gchar *unused, + const gchar *key) +{ + EPassMsg *msg; + gchar *passwd; + + g_return_val_if_fail (key != NULL, NULL); + + msg = ep_msg_new (ep_get_password); + msg->key = key; + + ep_msg_send (msg); + + passwd = msg->password; + msg->password = NULL; + ep_msg_free (msg); + + return passwd; +} + +/** + * e_passwords_add_password: + * @key: a key + * @passwd: the password for @key + * + * This stores the @key/@passwd pair in the current session's password + * hash. + **/ +void +e_passwords_add_password (const gchar *key, + const gchar *passwd) +{ + EPassMsg *msg; + + g_return_if_fail (key != NULL); + g_return_if_fail (passwd != NULL); + + msg = ep_msg_new (ep_add_password); + msg->key = key; + msg->oldpass = passwd; + + ep_msg_send (msg); + ep_msg_free (msg); +} + +/** + * e_passwords_ask_password: + * @title: title for the password dialog + * @unused: this argument is no longer used + * @key: key to store the password under + * @prompt: prompt string + * @remember_type: whether or not to offer to remember the password, + * and for how long. + * @remember: on input, the default state of the remember checkbox. + * on output, the state of the checkbox when the dialog was closed. + * @parent: parent window of the dialog, or %NULL + * + * Asks the user for a password. + * + * Returns: the password, which the caller must free, or %NULL if + * the user cancelled the operation. *@remember will be set if the + * return value is non-%NULL and @remember_type is not + * E_PASSWORDS_DO_NOT_REMEMBER. + **/ +gchar * +e_passwords_ask_password (const gchar *title, + const gchar *unused, + const gchar *key, + const gchar *prompt, + EPasswordsRememberType remember_type, + gboolean *remember, + GtkWindow *parent) +{ + gchar *passwd; + EPassMsg *msg; + + g_return_val_if_fail (key != NULL, NULL); + + if ((remember_type & E_PASSWORDS_ONLINE) && !ep_online_state) + return NULL; + + msg = ep_msg_new (ep_ask_password); + msg->title = title; + msg->key = key; + msg->prompt = prompt; + msg->flags = remember_type; + msg->remember = remember; + msg->parent = parent; + + ep_msg_send (msg); + passwd = msg->password; + msg->password = NULL; + ep_msg_free (msg); + + return passwd; +} diff --git a/e-util/e-passwords.h b/e-util/e-passwords.h new file mode 100644 index 0000000000..83a4a7eaec --- /dev/null +++ b/e-util/e-passwords.h @@ -0,0 +1,81 @@ +/* + * e-passwords.h + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef EDS_DISABLE_DEPRECATED + +#ifndef _E_PASSWORD_H_ +#define _E_PASSWORD_H_ + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +/* + * initialization is now implicit when you call any of the functions + * below, although this is only correct if the functions are called + * from the main thread. + * + * e_passwords_shutdown should be called at exit time to synch the + * password on-disk storage, and to free up in-memory storage. */ +void e_passwords_init (void); + +void e_passwords_shutdown (void); +void e_passwords_cancel (void); +void e_passwords_set_online (gint state); +void e_passwords_remember_password (const gchar *unused, const gchar *key); +void e_passwords_add_password (const gchar *key, const gchar *passwd); +gchar *e_passwords_get_password (const gchar *unused, const gchar *key); +void e_passwords_forget_password (const gchar *unused, const gchar *key); +void e_passwords_forget_passwords (void); +void e_passwords_clear_passwords (const gchar *unused); + +typedef enum { + E_PASSWORDS_REMEMBER_NEVER, + E_PASSWORDS_REMEMBER_SESSION, + E_PASSWORDS_REMEMBER_FOREVER, + E_PASSWORDS_REMEMBER_MASK = 0xf, + + /* option bits */ + E_PASSWORDS_SECRET = 1 << 8, + E_PASSWORDS_REPROMPT = 1 << 9, + E_PASSWORDS_ONLINE = 1<<10, /* only ask if we're online */ + E_PASSWORDS_DISABLE_REMEMBER = 1<<11, /* disable the 'remember password' checkbox */ + E_PASSWORDS_PASSPHRASE = 1<<12 /* We are asking a passphrase */ +} EPasswordsRememberType; + +gchar * e_passwords_ask_password (const gchar *title, + const gchar *unused, + const gchar *key, + const gchar *prompt, + EPasswordsRememberType remember_type, + gboolean *remember, + GtkWindow *parent); + +G_END_DECLS + +#endif /* _E_PASSWORD_H_ */ + +#endif /* EDS_DISABLE_DEPRECATED */ diff --git a/e-util/e-picture-gallery.c b/e-util/e-picture-gallery.c new file mode 100644 index 0000000000..d95a0c907c --- /dev/null +++ b/e-util/e-picture-gallery.c @@ -0,0 +1,437 @@ +/* + * e-picture-gallery.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-picture-gallery.h" + +#include "e-icon-factory.h" + +#define E_PICTURE_GALLERY_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_PICTURE_GALLERY, EPictureGalleryPrivate)) + +struct _EPictureGalleryPrivate { + gboolean initialized; + gchar *path; + GFileMonitor *monitor; +}; + +enum { + PROP_0, + PROP_PATH +}; + +enum { + COL_PIXBUF = 0, + COL_URI, + COL_FILENAME_TEXT +}; + +G_DEFINE_TYPE (EPictureGallery, e_picture_gallery, GTK_TYPE_ICON_VIEW) + +static gboolean +update_file_iter (GtkListStore *list_store, + GtkTreeIter *iter, + GFile *file, + gboolean force_thumbnail_update) +{ + GFileInfo *file_info; + gchar *uri; + gboolean res = FALSE; + + g_return_val_if_fail (list_store != NULL, FALSE); + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (file != NULL, FALSE); + + uri = g_file_get_uri (file); + + file_info = g_file_query_info ( + file, + G_FILE_ATTRIBUTE_THUMBNAIL_PATH "," + G_FILE_ATTRIBUTE_THUMBNAILING_FAILED "," + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," + G_FILE_ATTRIBUTE_STANDARD_SIZE, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + + if (file_info != NULL) { + const gchar *existing_thumb = g_file_info_get_attribute_byte_string (file_info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + gchar *new_thumb = NULL; + + if (!existing_thumb || force_thumbnail_update) { + gchar *filename; + + filename = g_file_get_path (file); + if (filename) { + new_thumb = e_icon_factory_create_thumbnail (filename); + if (new_thumb) + existing_thumb = new_thumb; + g_free (filename); + } + } + + if (existing_thumb && !g_file_info_get_attribute_boolean (file_info, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED)) { + GdkPixbuf * pixbuf; + + pixbuf = gdk_pixbuf_new_from_file (existing_thumb, NULL); + + if (pixbuf) { + const gchar *filename; + gchar *filename_text = NULL; + guint64 filesize; + + filename = g_file_info_get_attribute_string (file_info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME); + if (filename) { + filesize = g_file_info_get_attribute_uint64 (file_info, G_FILE_ATTRIBUTE_STANDARD_SIZE); + if (filesize) { + gchar *tmp = g_format_size ((goffset) filesize); + filename_text = g_strdup_printf ("%s (%s)", filename, tmp); + g_free (tmp); + } + + res = TRUE; + gtk_list_store_set ( + list_store, iter, + COL_PIXBUF, pixbuf, + COL_URI, uri, + COL_FILENAME_TEXT, filename_text ? filename_text : filename, + -1); + } + + g_object_unref (pixbuf); + g_free (filename_text); + } + } + + g_free (new_thumb); + } + + g_free (uri); + + return res; +} + +static void +add_file (GtkListStore *list_store, + GFile *file) +{ + GtkTreeIter iter; + + g_return_if_fail (list_store != NULL); + g_return_if_fail (file != NULL); + + gtk_list_store_append (list_store, &iter); + if (!update_file_iter (list_store, &iter, file, FALSE)) + gtk_list_store_remove (list_store, &iter); +} + +static gboolean +find_file_uri (GtkListStore *list_store, + const gchar *uri, + GtkTreeIter *iter) +{ + GtkTreeModel *model; + + g_return_val_if_fail (list_store != NULL, FALSE); + g_return_val_if_fail (uri != NULL, FALSE); + g_return_val_if_fail (iter != NULL, FALSE); + + model = GTK_TREE_MODEL (list_store); + g_return_val_if_fail (model != NULL, FALSE); + + if (!gtk_tree_model_get_iter_first (model, iter)) + return FALSE; + + do { + gchar *iter_uri = NULL; + + gtk_tree_model_get ( + model, iter, + COL_URI, &iter_uri, + -1); + + if (iter_uri && g_ascii_strcasecmp (uri, iter_uri) == 0) { + g_free (iter_uri); + return TRUE; + } + + g_free (iter_uri); + } while (gtk_tree_model_iter_next (model, iter)); + + return FALSE; +} + +static void +picture_gallery_dir_changed_cb (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + EPictureGallery *gallery) +{ + gchar *uri; + GtkListStore *list_store; + GtkTreeIter iter; + + g_return_if_fail (file != NULL); + + list_store = GTK_LIST_STORE (gtk_icon_view_get_model (GTK_ICON_VIEW (gallery))); + g_return_if_fail (list_store != NULL); + + uri = g_file_get_uri (file); + if (!uri) + return; + + switch (event_type) { + case G_FILE_MONITOR_EVENT_CREATED: + if (find_file_uri (list_store, uri, &iter)) { + if (!update_file_iter (list_store, &iter, file, TRUE)) + gtk_list_store_remove (list_store, &iter); + } else { + add_file (list_store, file); + } + break; + case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: + if (find_file_uri (list_store, uri, &iter)) { + if (!update_file_iter (list_store, &iter, file, TRUE)) + gtk_list_store_remove (list_store, &iter); + } + break; + case G_FILE_MONITOR_EVENT_DELETED: + if (find_file_uri (list_store, uri, &iter)) + gtk_list_store_remove (list_store, &iter); + break; + default: + break; + } + + g_free (uri); +} + +static gboolean +picture_gallery_start_loading_cb (EPictureGallery *gallery) +{ + GtkIconView *icon_view; + GtkListStore *list_store; + GDir *dir; + const gchar *dirname; + + icon_view = GTK_ICON_VIEW (gallery); + list_store = GTK_LIST_STORE (gtk_icon_view_get_model (icon_view)); + g_return_val_if_fail (list_store != NULL, FALSE); + + dirname = e_picture_gallery_get_path (gallery); + if (!dirname) + return FALSE; + + dir = g_dir_open (dirname, 0, NULL); + if (dir) { + GFile *file; + const gchar *basename; + + while ((basename = g_dir_read_name (dir)) != NULL) { + gchar *filename; + + filename = g_build_filename (dirname, basename, NULL); + file = g_file_new_for_path (filename); + + add_file (list_store, file); + + g_free (filename); + g_object_unref (file); + } + + g_dir_close (dir); + + file = g_file_new_for_path (dirname); + gallery->priv->monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, NULL, NULL); + g_object_unref (file); + + if (gallery->priv->monitor) + g_signal_connect ( + gallery->priv->monitor, "changed", + G_CALLBACK (picture_gallery_dir_changed_cb), + gallery); + } + + g_object_unref (icon_view); + + return FALSE; +} + +const gchar * +e_picture_gallery_get_path (EPictureGallery *gallery) +{ + g_return_val_if_fail (gallery != NULL, NULL); + g_return_val_if_fail (E_IS_PICTURE_GALLERY (gallery), NULL); + g_return_val_if_fail (gallery->priv != NULL, NULL); + + return gallery->priv->path; +} + +static void +picture_gallery_set_path (EPictureGallery *gallery, + const gchar *path) +{ + g_return_if_fail (E_IS_PICTURE_GALLERY (gallery)); + g_return_if_fail (gallery->priv != NULL); + + g_free (gallery->priv->path); + + if (!path || !*path || !g_file_test (path, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) + gallery->priv->path = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_PICTURES)); + else + gallery->priv->path = g_strdup (path); +} + +static void +picture_gallery_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_PATH: + g_value_set_string (value, e_picture_gallery_get_path (E_PICTURE_GALLERY (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +picture_gallery_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_PATH: + picture_gallery_set_path (E_PICTURE_GALLERY (object), g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +visible_cb (EPictureGallery *gallery) +{ + if (!gallery->priv->initialized && gtk_widget_get_visible (GTK_WIDGET (gallery))) { + gallery->priv->initialized = TRUE; + + g_idle_add ((GSourceFunc) picture_gallery_start_loading_cb, gallery); + } +} + +static void +picture_gallery_constructed (GObject *object) +{ + GtkIconView *icon_view; + GtkListStore *list_store; + GtkTargetEntry *targets; + GtkTargetList *list; + gint n_targets; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_picture_gallery_parent_class)->constructed (object); + + icon_view = GTK_ICON_VIEW (object); + + list_store = gtk_list_store_new (3, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING); + gtk_icon_view_set_model (icon_view, GTK_TREE_MODEL (list_store)); + g_object_unref (list_store); + + gtk_icon_view_set_pixbuf_column (icon_view, COL_PIXBUF); + gtk_icon_view_set_text_column (icon_view, COL_FILENAME_TEXT); + gtk_icon_view_set_tooltip_column (icon_view, -1); + + list = gtk_target_list_new (NULL, 0); + gtk_target_list_add_uri_targets (list, 0); + targets = gtk_target_table_new_from_list (list, &n_targets); + + gtk_icon_view_enable_model_drag_source ( + icon_view, GDK_BUTTON1_MASK, + targets, n_targets, GDK_ACTION_COPY); + + gtk_target_table_free (targets, n_targets); + gtk_target_list_unref (list); + + g_signal_connect (object, "notify::visible", G_CALLBACK (visible_cb), NULL); +} + +static void +picture_gallery_dispose (GObject *object) +{ + EPictureGallery *gallery; + + gallery = E_PICTURE_GALLERY (object); + + if (gallery->priv->monitor) { + g_object_unref (gallery->priv->monitor); + gallery->priv->monitor = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_picture_gallery_parent_class)->dispose (object); +} + +static void +e_picture_gallery_class_init (EPictureGalleryClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EPictureGalleryPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = picture_gallery_get_property; + object_class->set_property = picture_gallery_set_property; + object_class->constructed = picture_gallery_constructed; + object_class->dispose = picture_gallery_dispose; + + g_object_class_install_property ( + object_class, + PROP_PATH, + g_param_spec_string ( + "path", + "Gallery path", + NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +e_picture_gallery_init (EPictureGallery *gallery) +{ + gallery->priv = E_PICTURE_GALLERY_GET_PRIVATE (gallery); + gallery->priv->initialized = FALSE; + gallery->priv->monitor = NULL; + picture_gallery_set_path (gallery, NULL); +} + +GtkWidget * +e_picture_gallery_new (const gchar *path) +{ + return g_object_new (E_TYPE_PICTURE_GALLERY, "path", path, NULL); +} diff --git a/e-util/e-picture-gallery.h b/e-util/e-picture-gallery.h new file mode 100644 index 0000000000..653d9906af --- /dev/null +++ b/e-util/e-picture-gallery.h @@ -0,0 +1,71 @@ +/* + * e-picture-gallery.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_PICTURE_GALLERY_H +#define E_PICTURE_GALLERY_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_PICTURE_GALLERY \ + (e_picture_gallery_get_type ()) +#define E_PICTURE_GALLERY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_PICTURE_GALLERY, EPictureGallery)) +#define E_PICTURE_GALLERY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_PICTURE_GALLERY, EPictureGalleryClass)) +#define E_IS_PICTURE_GALLERY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_PICTURE_GALLERY)) +#define E_IS_PICTURE_GALLERY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_PICTURE_GALLERY)) +#define E_PICTURE_GALLERY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_PICTURE_GALLERY, EPictureGalleryClass)) + +G_BEGIN_DECLS + +typedef struct _EPictureGallery EPictureGallery; +typedef struct _EPictureGalleryClass EPictureGalleryClass; +typedef struct _EPictureGalleryPrivate EPictureGalleryPrivate; + +struct _EPictureGallery { + GtkIconView parent; + EPictureGalleryPrivate *priv; +}; + +struct _EPictureGalleryClass { + GtkIconViewClass parent_class; +}; + +GType e_picture_gallery_get_type (void); +GtkWidget * e_picture_gallery_new (const gchar *path); +const gchar * e_picture_gallery_get_path (EPictureGallery *gallery); + +G_END_DECLS + +#endif /* E_PICTURE_GALLERY_H */ diff --git a/e-util/e-plugin-ui.c b/e-util/e-plugin-ui.c index 6e36654061..3ef863c2a3 100644 --- a/e-util/e-plugin-ui.c +++ b/e-util/e-plugin-ui.c @@ -21,7 +21,6 @@ #include "e-plugin-ui.h" -#include "e-util.h" #include "e-ui-manager.h" #include <string.h> diff --git a/e-util/e-plugin-ui.h b/e-util/e-plugin-ui.h index e59b5f5222..f56a6e095c 100644 --- a/e-util/e-plugin-ui.h +++ b/e-util/e-plugin-ui.h @@ -15,6 +15,10 @@ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_PLUGIN_UI_H #define E_PLUGIN_UI_H diff --git a/e-util/e-plugin.h b/e-util/e-plugin.h index 047d944193..b67bde548c 100644 --- a/e-util/e-plugin.h +++ b/e-util/e-plugin.h @@ -19,6 +19,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef _E_PLUGIN_H #define _E_PLUGIN_H diff --git a/e-util/e-poolv.h b/e-util/e-poolv.h index e3cfb31007..f1b4654127 100644 --- a/e-util/e-poolv.h +++ b/e-util/e-poolv.h @@ -16,6 +16,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_POOLV_H #define E_POOLV_H diff --git a/e-util/e-popup-action.c b/e-util/e-popup-action.c new file mode 100644 index 0000000000..27c90f67c3 --- /dev/null +++ b/e-util/e-popup-action.c @@ -0,0 +1,408 @@ +/* + * e-popup-action.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-popup-action.h" + +#include <glib/gi18n.h> + +#define E_POPUP_ACTION_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_POPUP_ACTION, EPopupActionPrivate)) + +enum { + PROP_0, + PROP_RELATED_ACTION, + PROP_USE_ACTION_APPEARANCE +}; + +struct _EPopupActionPrivate { + GtkAction *related_action; + gboolean use_action_appearance; + gulong activate_handler_id; + gulong notify_handler_id; +}; + +/* Forward Declarations */ +static void e_popup_action_activatable_init (GtkActivatableIface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EPopupAction, + e_popup_action, + GTK_TYPE_ACTION, + G_IMPLEMENT_INTERFACE ( + GTK_TYPE_ACTIVATABLE, + e_popup_action_activatable_init)) + +static void +popup_action_notify_cb (GtkAction *action, + GParamSpec *pspec, + GtkActivatable *activatable) +{ + GtkActivatableIface *iface; + + iface = GTK_ACTIVATABLE_GET_IFACE (activatable); + g_return_if_fail (iface->update != NULL); + + iface->update (activatable, action, pspec->name); +} + +static GtkAction * +popup_action_get_related_action (EPopupAction *popup_action) +{ + return popup_action->priv->related_action; +} + +static void +popup_action_set_related_action (EPopupAction *popup_action, + GtkAction *related_action) +{ + GtkActivatable *activatable; + + /* Do not call gtk_activatable_do_set_related_action() because + * it assumes the activatable object is a widget and tries to add + * it to the related actions's proxy list. Instead we'll just do + * the relevant steps manually. */ + + activatable = GTK_ACTIVATABLE (popup_action); + + if (related_action == popup_action->priv->related_action) + return; + + if (related_action != NULL) + g_object_ref (related_action); + + if (popup_action->priv->related_action != NULL) { + g_signal_handler_disconnect ( + popup_action, + popup_action->priv->activate_handler_id); + g_signal_handler_disconnect ( + popup_action->priv->related_action, + popup_action->priv->notify_handler_id); + popup_action->priv->activate_handler_id = 0; + popup_action->priv->notify_handler_id = 0; + g_object_unref (popup_action->priv->related_action); + } + + popup_action->priv->related_action = related_action; + + if (related_action != NULL) { + popup_action->priv->activate_handler_id = + g_signal_connect_swapped ( + popup_action, "activate", + G_CALLBACK (gtk_action_activate), + related_action); + popup_action->priv->notify_handler_id = + g_signal_connect ( + related_action, "notify", + G_CALLBACK (popup_action_notify_cb), + popup_action); + gtk_activatable_sync_action_properties ( + activatable, related_action); + } else + gtk_action_set_visible (GTK_ACTION (popup_action), FALSE); + + g_object_notify (G_OBJECT (popup_action), "related-action"); +} + +static gboolean +popup_action_get_use_action_appearance (EPopupAction *popup_action) +{ + return popup_action->priv->use_action_appearance; +} + +static void +popup_action_set_use_action_appearance (EPopupAction *popup_action, + gboolean use_action_appearance) +{ + GtkActivatable *activatable; + GtkAction *related_action; + + if (popup_action->priv->use_action_appearance == use_action_appearance) + return; + + popup_action->priv->use_action_appearance = use_action_appearance; + + g_object_notify (G_OBJECT (popup_action), "use-action-appearance"); + + activatable = GTK_ACTIVATABLE (popup_action); + related_action = popup_action_get_related_action (popup_action); + gtk_activatable_sync_action_properties (activatable, related_action); +} + +static void +popup_action_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_RELATED_ACTION: + popup_action_set_related_action ( + E_POPUP_ACTION (object), + g_value_get_object (value)); + return; + + case PROP_USE_ACTION_APPEARANCE: + popup_action_set_use_action_appearance ( + E_POPUP_ACTION (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +popup_action_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_RELATED_ACTION: + g_value_set_object ( + value, + popup_action_get_related_action ( + E_POPUP_ACTION (object))); + return; + + case PROP_USE_ACTION_APPEARANCE: + g_value_set_boolean ( + value, + popup_action_get_use_action_appearance ( + E_POPUP_ACTION (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +popup_action_dispose (GObject *object) +{ + EPopupActionPrivate *priv; + + priv = E_POPUP_ACTION_GET_PRIVATE (object); + + if (priv->related_action != NULL) { + g_signal_handler_disconnect ( + object, + priv->activate_handler_id); + g_signal_handler_disconnect ( + priv->related_action, + priv->notify_handler_id); + g_object_unref (priv->related_action); + priv->related_action = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_popup_action_parent_class)->dispose (object); +} + +static void +popup_action_update (GtkActivatable *activatable, + GtkAction *action, + const gchar *property_name) +{ + GObjectClass *class; + GParamSpec *pspec; + GValue *value; + + /* Ignore "action-group" changes" */ + if (strcmp (property_name, "action-group") == 0) + return; + + /* Ignore "visible" changes. */ + if (strcmp (property_name, "visible") == 0) + return; + + value = g_slice_new0 (GValue); + class = G_OBJECT_GET_CLASS (action); + pspec = g_object_class_find_property (class, property_name); + g_value_init (value, pspec->value_type); + + g_object_get_property (G_OBJECT (action), property_name, value); + + if (strcmp (property_name, "sensitive") == 0) + property_name = "visible"; + else if (!gtk_activatable_get_use_action_appearance (activatable)) + goto exit; + + g_object_set_property (G_OBJECT (activatable), property_name, value); + +exit: + g_value_unset (value); + g_slice_free (GValue, value); +} + +static void +popup_action_sync_action_properties (GtkActivatable *activatable, + GtkAction *action) +{ + if (action == NULL) + return; + + /* XXX GTK+ 2.18 is still missing accessor functions for + * "hide-if-empty" and "visible-overflown" properties. + * These are rarely used so we'll skip them for now. */ + + /* A popup action is never shown as insensitive. */ + gtk_action_set_sensitive (GTK_ACTION (activatable), TRUE); + + gtk_action_set_visible ( + GTK_ACTION (activatable), + gtk_action_get_sensitive (action)); + + gtk_action_set_visible_horizontal ( + GTK_ACTION (activatable), + gtk_action_get_visible_horizontal (action)); + + gtk_action_set_visible_vertical ( + GTK_ACTION (activatable), + gtk_action_get_visible_vertical (action)); + + gtk_action_set_is_important ( + GTK_ACTION (activatable), + gtk_action_get_is_important (action)); + + if (!gtk_activatable_get_use_action_appearance (activatable)) + return; + + gtk_action_set_label ( + GTK_ACTION (activatable), + gtk_action_get_label (action)); + + gtk_action_set_short_label ( + GTK_ACTION (activatable), + gtk_action_get_short_label (action)); + + gtk_action_set_tooltip ( + GTK_ACTION (activatable), + gtk_action_get_tooltip (action)); + + gtk_action_set_stock_id ( + GTK_ACTION (activatable), + gtk_action_get_stock_id (action)); + + gtk_action_set_gicon ( + GTK_ACTION (activatable), + gtk_action_get_gicon (action)); + + gtk_action_set_icon_name ( + GTK_ACTION (activatable), + gtk_action_get_icon_name (action)); +} + +static void +e_popup_action_class_init (EPopupActionClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EPopupActionPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = popup_action_set_property; + object_class->get_property = popup_action_get_property; + object_class->dispose = popup_action_dispose; + + g_object_class_override_property ( + object_class, + PROP_RELATED_ACTION, + "related-action"); + + g_object_class_override_property ( + object_class, + PROP_USE_ACTION_APPEARANCE, + "use-action-appearance"); +} + +static void +e_popup_action_init (EPopupAction *popup_action) +{ + popup_action->priv = E_POPUP_ACTION_GET_PRIVATE (popup_action); + popup_action->priv->use_action_appearance = TRUE; + + /* Remain invisible until we have a related action. */ + gtk_action_set_visible (GTK_ACTION (popup_action), FALSE); +} + +static void +e_popup_action_activatable_init (GtkActivatableIface *interface) +{ + interface->update = popup_action_update; + interface->sync_action_properties = popup_action_sync_action_properties; +} + +EPopupAction * +e_popup_action_new (const gchar *name) +{ + g_return_val_if_fail (name != NULL, NULL); + + return g_object_new (E_TYPE_POPUP_ACTION, "name", name, NULL); +} + +void +e_action_group_add_popup_actions (GtkActionGroup *action_group, + const EPopupActionEntry *entries, + guint n_entries) +{ + guint ii; + + g_return_if_fail (GTK_IS_ACTION_GROUP (action_group)); + + for (ii = 0; ii < n_entries; ii++) { + EPopupAction *popup_action; + GtkAction *related_action; + const gchar *label; + + label = gtk_action_group_translate_string ( + action_group, entries[ii].label); + + related_action = gtk_action_group_get_action ( + action_group, entries[ii].related); + + if (related_action == NULL) { + g_warning ( + "Related action '%s' not found in " + "action group '%s'", entries[ii].related, + gtk_action_group_get_name (action_group)); + continue; + } + + popup_action = e_popup_action_new (entries[ii].name); + + gtk_activatable_set_related_action ( + GTK_ACTIVATABLE (popup_action), related_action); + + if (label != NULL && *label != '\0') + gtk_action_set_label ( + GTK_ACTION (popup_action), label); + + gtk_action_group_add_action ( + action_group, GTK_ACTION (popup_action)); + + g_object_unref (popup_action); + } +} diff --git a/e-util/e-popup-action.h b/e-util/e-popup-action.h new file mode 100644 index 0000000000..62e7b8ec0f --- /dev/null +++ b/e-util/e-popup-action.h @@ -0,0 +1,96 @@ +/* + * e-popup-action.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* A popup action is an action that lives in a popup menu. It proxies an + * equivalent action in the main menu, with two differences: + * + * 1) If the main menu action is insensitive, the popup action is invisible. + * 2) The popup action may have a different label than the main menu action. + * + * To use: + * + * Create an array of EPopupActionEntry structs. Add the main menu actions + * that serve as related actions for the popup actions to an action group + * first. Then pass the same action group and the EPopupActionEntry array + * to e_action_group_add_popup_actions() to add popup actions. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_POPUP_ACTION_H +#define E_POPUP_ACTION_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_POPUP_ACTION \ + (e_popup_action_get_type ()) +#define E_POPUP_ACTION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_POPUP_ACTION, EPopupAction)) +#define E_POPUP_ACTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_POPUP_ACTION, EPopupActionClass)) +#define E_IS_POPUP_ACTION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_POPUP_ACTION)) +#define E_IS_POPUP_ACTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_POPUP_ACTION)) +#define E_POPUP_ACTION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_POPUP_ACTION, EPopupActionClass)) + +G_BEGIN_DECLS + +typedef struct _EPopupAction EPopupAction; +typedef struct _EPopupActionClass EPopupActionClass; +typedef struct _EPopupActionPrivate EPopupActionPrivate; +typedef struct _EPopupActionEntry EPopupActionEntry; + +struct _EPopupAction { + GtkAction parent; + EPopupActionPrivate *priv; +}; + +struct _EPopupActionClass { + GtkActionClass parent_class; +}; + +struct _EPopupActionEntry { + const gchar *name; + const gchar *label; /* optional: overrides the related action */ + const gchar *related; /* name of the related action */ +}; + +GType e_popup_action_get_type (void); +EPopupAction * e_popup_action_new (const gchar *name); + +void e_action_group_add_popup_actions + (GtkActionGroup *action_group, + const EPopupActionEntry *entries, + guint n_entries); + +G_END_DECLS + +#endif /* E_POPUP_ACTION_H */ diff --git a/e-util/e-popup-menu.c b/e-util/e-popup-menu.c new file mode 100644 index 0000000000..3e82496748 --- /dev/null +++ b/e-util/e-popup-menu.c @@ -0,0 +1,112 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * Jody Goldberg (jgoldberg@home.com) + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <libintl.h> +#include <string.h> + +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> + +#include "e-popup-menu.h" + +/* + * Creates an item with an optional icon + */ +static void +make_item (GtkMenu *menu, + GtkMenuItem *item, + const gchar *name) +{ + GtkWidget *label; + + if (*name == '\0') + return; + + /* + * Ugh. This needs to go into Gtk+ + */ + label = gtk_label_new_with_mnemonic (name); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_widget_show (label); + + gtk_container_add (GTK_CONTAINER (item), label); +} + +GtkMenu * +e_popup_menu_create_with_domain (EPopupMenu *menu_list, + guint32 disable_mask, + guint32 hide_mask, + gpointer default_closure, + const gchar *domain) +{ + GtkMenu *menu = GTK_MENU (gtk_menu_new ()); + gboolean last_item_separator = TRUE; + gint last_non_separator = -1; + gint i; + + for (i = 0; menu_list[i].name; i++) { + if (strcmp ("", menu_list[i].name) && !(menu_list[i].disable_mask & hide_mask)) { + last_non_separator = i; + } + } + + for (i = 0; i <= last_non_separator; i++) { + gboolean separator; + + separator = !strcmp ("", menu_list[i].name); + + if ((!(separator && last_item_separator)) && !(menu_list[i].disable_mask & hide_mask)) { + GtkWidget *item = NULL; + + if (!separator) { + item = gtk_menu_item_new (); + + make_item (menu, GTK_MENU_ITEM (item), dgettext (domain, menu_list[i].name)); + } else { + item = gtk_menu_item_new (); + } + + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + + if (menu_list[i].fn) + g_signal_connect ( + item, "activate", + G_CALLBACK (menu_list[i].fn), + default_closure); + + if (menu_list[i].disable_mask & disable_mask) + gtk_widget_set_sensitive (item, FALSE); + + gtk_widget_show (item); + + last_item_separator = separator; + } + } + + return menu; +} diff --git a/e-util/e-popup-menu.h b/e-util/e-popup-menu.h new file mode 100644 index 0000000000..ec0979c576 --- /dev/null +++ b/e-util/e-popup-menu.h @@ -0,0 +1,59 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * Jody Goldberg (jgoldberg@home.com) + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_POPUP_MENU_H +#define E_POPUP_MENU_H + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define E_POPUP_SEPARATOR { (gchar *) "", NULL, (NULL), 0 } +#define E_POPUP_TERMINATOR { NULL, NULL, (NULL), 0 } + +#define E_POPUP_ITEM(name,fn,disable_mask) \ + { (gchar *) (name), NULL, (fn), (disable_mask) } + +typedef struct _EPopupMenu EPopupMenu; + +struct _EPopupMenu { + gchar *name; + gchar *pixname; + GCallback fn; + guint32 disable_mask; +}; + +GtkMenu * e_popup_menu_create_with_domain (EPopupMenu *menu_list, + guint32 disable_mask, + guint32 hide_mask, + gpointer default_closure, + const gchar *domain); + +G_END_DECLS + +#endif /* E_POPUP_MENU_H */ diff --git a/e-util/e-port-entry.c b/e-util/e-port-entry.c new file mode 100644 index 0000000000..cf857b5d55 --- /dev/null +++ b/e-util/e-port-entry.c @@ -0,0 +1,549 @@ +/* + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * Authors: + * Dan Vratil <dvratil@redhat.com> + */ + +#include "e-port-entry.h" + +#include <config.h> +#include <errno.h> +#include <stddef.h> +#include <string.h> + +#define E_PORT_ENTRY_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_PORT_ENTRY, EPortEntryPrivate)) + +struct _EPortEntryPrivate { + CamelNetworkSecurityMethod method; + CamelProviderPortEntry *entries; +}; + +enum { + PORT_NUM_COLUMN, + PORT_DESC_COLUMN, + PORT_IS_SSL_COLUMN +}; + +enum { + PROP_0, + PROP_IS_VALID, + PROP_PORT, + PROP_SECURITY_METHOD +}; + +G_DEFINE_TYPE ( + EPortEntry, + e_port_entry, + GTK_TYPE_COMBO_BOX) + +static GtkEntry * +port_entry_get_entry (EPortEntry *port_entry) +{ + return GTK_ENTRY (gtk_bin_get_child (GTK_BIN (port_entry))); +} + +static gboolean +port_entry_get_numeric_port (EPortEntry *port_entry, + gint *out_port) +{ + GtkEntry *entry; + const gchar *port_string; + gboolean valid; + gint port; + + entry = port_entry_get_entry (port_entry); + + port_string = gtk_entry_get_text (entry); + g_return_val_if_fail (port_string != NULL, FALSE); + + errno = 0; + port = strtol (port_string, NULL, 10); + valid = (errno == 0) && (port == CLAMP (port, 1, G_MAXUINT16)); + + if (valid && out_port != NULL) + *out_port = port; + + return valid; +} + +static void +port_entry_text_changed (GtkEditable *editable, + EPortEntry *port_entry) +{ + GObject *object = G_OBJECT (port_entry); + const gchar *desc = NULL; + gint port = 0; + gint ii = 0; + + g_object_freeze_notify (object); + + port_entry_get_numeric_port (port_entry, &port); + + if (port_entry->priv->entries != NULL) { + while (port_entry->priv->entries[ii].port > 0) { + if (port == port_entry->priv->entries[ii].port) { + desc = port_entry->priv->entries[ii].desc; + break; + } + ii++; + } + } + + if (desc != NULL) + gtk_widget_set_tooltip_text (GTK_WIDGET (port_entry), desc); + else + gtk_widget_set_has_tooltip (GTK_WIDGET (port_entry), FALSE); + + g_object_notify (object, "port"); + g_object_notify (object, "is-valid"); + + g_object_thaw_notify (object); +} + +static void +port_entry_method_changed (EPortEntry *port_entry) +{ + CamelNetworkSecurityMethod method; + gboolean standard_port = FALSE; + gboolean valid, have_ssl = FALSE, have_nossl = FALSE; + gint port = 0; + gint ii; + + method = e_port_entry_get_security_method (port_entry); + valid = port_entry_get_numeric_port (port_entry, &port); + + /* Only change the port number if it's currently on a standard + * port (i.e. listed in a CamelProviderPortEntry). Otherwise, + * leave custom port numbers alone. */ + + if (valid && port_entry->priv->entries != NULL) { + for (ii = 0; port_entry->priv->entries[ii].port > 0 && (!have_ssl || !have_nossl); ii++) { + /* Use only the first SSL/no-SSL port as a default in the list + * and skip the others */ + if (port_entry->priv->entries[ii].is_ssl) { + if (have_ssl) + continue; + have_ssl = TRUE; + } else { + if (have_nossl) + continue; + have_nossl = TRUE; + } + + if (port == port_entry->priv->entries[ii].port) { + standard_port = TRUE; + break; + } + } + } + + if (valid && !standard_port) + return; + + switch (method) { + case CAMEL_NETWORK_SECURITY_METHOD_SSL_ON_ALTERNATE_PORT: + e_port_entry_activate_secured_port (port_entry, 0); + break; + default: + e_port_entry_activate_nonsecured_port (port_entry, 0); + break; + } +} + +static void +port_entry_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_PORT: + e_port_entry_set_port ( + E_PORT_ENTRY (object), + g_value_get_uint (value)); + return; + + case PROP_SECURITY_METHOD: + e_port_entry_set_security_method ( + E_PORT_ENTRY (object), + g_value_get_enum (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +port_entry_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_IS_VALID: + g_value_set_boolean ( + value, e_port_entry_is_valid ( + E_PORT_ENTRY (object))); + return; + + case PROP_PORT: + g_value_set_uint ( + value, e_port_entry_get_port ( + E_PORT_ENTRY (object))); + return; + + case PROP_SECURITY_METHOD: + g_value_set_enum ( + value, e_port_entry_get_security_method ( + E_PORT_ENTRY (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +port_entry_constructed (GObject *object) +{ + GtkEntry *entry; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_port_entry_parent_class)->constructed (object); + + entry = port_entry_get_entry (E_PORT_ENTRY (object)); + + g_signal_connect_after ( + entry, "changed", + G_CALLBACK (port_entry_text_changed), object); +} + +static void +port_entry_get_preferred_width (GtkWidget *widget, + gint *minimum_size, + gint *natural_size) +{ + PangoContext *context; + PangoFontMetrics *metrics; + PangoFontDescription *font_desc; + GtkStyleContext *style_context; + GtkStateFlags state; + gint digit_width; + gint parent_entry_width_min; + gint parent_width_min; + GtkWidget *entry; + + style_context = gtk_widget_get_style_context (widget); + state = gtk_widget_get_state_flags (widget); + gtk_style_context_get ( + style_context, state, "font", &font_desc, NULL); + context = gtk_widget_get_pango_context (GTK_WIDGET (widget)); + metrics = pango_context_get_metrics ( + context, font_desc, pango_context_get_language (context)); + + digit_width = PANGO_PIXELS ( + pango_font_metrics_get_approximate_digit_width (metrics)); + + /* Preferred width of the entry */ + entry = gtk_bin_get_child (GTK_BIN (widget)); + gtk_widget_get_preferred_width (entry, NULL, &parent_entry_width_min); + + /* Preferred width of a standard combobox */ + GTK_WIDGET_CLASS (e_port_entry_parent_class)-> + get_preferred_width (widget, &parent_width_min, NULL); + + /* 6 * digit_width - port number has max 5 + * digits + extra free space for better look */ + if (minimum_size != NULL) + *minimum_size = + parent_width_min - parent_entry_width_min + + 6 * digit_width; + + if (natural_size != NULL) + *natural_size = + parent_width_min - parent_entry_width_min + + 6 * digit_width; + + pango_font_metrics_unref (metrics); + pango_font_description_free (font_desc); +} + +static void +e_port_entry_class_init (EPortEntryClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EPortEntryPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = port_entry_set_property; + object_class->get_property = port_entry_get_property; + object_class->constructed = port_entry_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->get_preferred_width = port_entry_get_preferred_width; + + g_object_class_install_property ( + object_class, + PROP_IS_VALID, + g_param_spec_boolean ( + "is-valid", + NULL, + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_PORT, + g_param_spec_uint ( + "port", + NULL, + NULL, + 0, /* Min port, 0 = invalid port */ + G_MAXUINT16, /* Max port */ + 0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SECURITY_METHOD, + g_param_spec_enum ( + "security-method", + "Security Method", + "Method used to establish a network connection", + CAMEL_TYPE_NETWORK_SECURITY_METHOD, + CAMEL_NETWORK_SECURITY_METHOD_NONE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_port_entry_init (EPortEntry *port_entry) +{ + GtkCellRenderer *renderer; + GtkListStore *store; + + port_entry->priv = E_PORT_ENTRY_GET_PRIVATE (port_entry); + + store = gtk_list_store_new ( + 3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN); + + gtk_combo_box_set_model ( + GTK_COMBO_BOX (port_entry), GTK_TREE_MODEL (store)); + gtk_combo_box_set_entry_text_column ( + GTK_COMBO_BOX (port_entry), PORT_NUM_COLUMN); + gtk_combo_box_set_id_column ( + GTK_COMBO_BOX (port_entry), PORT_NUM_COLUMN); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_renderer_set_sensitive (renderer, TRUE); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (port_entry), renderer, FALSE); + gtk_cell_layout_add_attribute ( + GTK_CELL_LAYOUT (port_entry), + renderer, "text", PORT_NUM_COLUMN); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_renderer_set_sensitive (renderer, FALSE); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (port_entry), renderer, TRUE); + gtk_cell_layout_add_attribute ( + GTK_CELL_LAYOUT (port_entry), + renderer, "text", PORT_DESC_COLUMN); +} + +GtkWidget * +e_port_entry_new (void) +{ + return g_object_new ( + E_TYPE_PORT_ENTRY, "has-entry", TRUE, NULL); +} + +void +e_port_entry_set_camel_entries (EPortEntry *port_entry, + CamelProviderPortEntry *entries) +{ + GtkComboBox *combo_box; + GtkTreeIter iter; + GtkTreeModel *model; + GtkListStore *store; + gint port = 0; + gint i = 0; + + g_return_if_fail (E_IS_PORT_ENTRY (port_entry)); + g_return_if_fail (entries); + + port_entry->priv->entries = entries; + + combo_box = GTK_COMBO_BOX (port_entry); + model = gtk_combo_box_get_model (combo_box); + + store = GTK_LIST_STORE (model); + gtk_list_store_clear (store); + + while (entries[i].port > 0) { + gchar *port_string; + + /* Grab the first port number. */ + if (port == 0) + port = entries[i].port; + + port_string = g_strdup_printf ("%i", entries[i].port); + + gtk_list_store_append (store, &iter); + gtk_list_store_set ( + store, &iter, + PORT_NUM_COLUMN, port_string, + PORT_DESC_COLUMN, entries[i].desc, + PORT_IS_SSL_COLUMN, entries[i].is_ssl, + -1); + i++; + + g_free (port_string); + } + + e_port_entry_set_port (port_entry, port); +} + +gint +e_port_entry_get_port (EPortEntry *port_entry) +{ + gint port = 0; + + g_return_val_if_fail (E_IS_PORT_ENTRY (port_entry), 0); + + port_entry_get_numeric_port (port_entry, &port); + + return port; +} + +void +e_port_entry_set_port (EPortEntry *port_entry, + gint port) +{ + GtkEntry *entry; + gchar *port_string; + + g_return_if_fail (E_IS_PORT_ENTRY (port_entry)); + + entry = port_entry_get_entry (port_entry); + port_string = g_strdup_printf ("%i", port); + gtk_entry_set_text (entry, port_string); + g_free (port_string); +} + +gboolean +e_port_entry_is_valid (EPortEntry *port_entry) +{ + g_return_val_if_fail (E_IS_PORT_ENTRY (port_entry), FALSE); + + return port_entry_get_numeric_port (port_entry, NULL); +} + +CamelNetworkSecurityMethod +e_port_entry_get_security_method (EPortEntry *port_entry) +{ + g_return_val_if_fail ( + E_IS_PORT_ENTRY (port_entry), + CAMEL_NETWORK_SECURITY_METHOD_NONE); + + return port_entry->priv->method; +} + +void +e_port_entry_set_security_method (EPortEntry *port_entry, + CamelNetworkSecurityMethod method) +{ + g_return_if_fail (E_IS_PORT_ENTRY (port_entry)); + + port_entry->priv->method = method; + + port_entry_method_changed (port_entry); + + g_object_notify (G_OBJECT (port_entry), "security-method"); +} + +/** + * If there are more then one secured port in the model, you can specify + * which of the secured ports should be activated by specifying the index. + * The index counts only for secured ports, so if you have 5 ports of which + * ports 1, 3 and 5 are secured, the association is 0=>1, 1=>3, 2=>5 + */ +void +e_port_entry_activate_secured_port (EPortEntry *port_entry, + gint index) +{ + GtkTreeModel *model; + GtkTreeIter iter; + gboolean is_ssl; + gint iters = 0; + + g_return_if_fail (E_IS_PORT_ENTRY (port_entry)); + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (port_entry)); + + if (!gtk_tree_model_get_iter_first (model, &iter)) + return; + + do { + gtk_tree_model_get ( + model, &iter, PORT_IS_SSL_COLUMN, &is_ssl, -1); + if (is_ssl && (iters == index)) { + gtk_combo_box_set_active_iter ( + GTK_COMBO_BOX (port_entry), &iter); + return; + } + + if (is_ssl) + iters++; + + } while (gtk_tree_model_iter_next (model, &iter)); +} + +/** + * If there are more then one unsecured port in the model, you can specify + * which of the unsecured ports should be activated by specifiying the index. + * The index counts only for unsecured ports, so if you have 5 ports, of which + * ports 2 and 4 are unsecured, the associtation is 0=>2, 1=>4 + */ +void +e_port_entry_activate_nonsecured_port (EPortEntry *port_entry, + gint index) +{ + GtkTreeModel *model; + GtkTreeIter iter; + gboolean is_ssl; + gint iters = 0; + + g_return_if_fail (E_IS_PORT_ENTRY (port_entry)); + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (port_entry)); + + if (!gtk_tree_model_get_iter_first (model, &iter)) + return; + + do { + gtk_tree_model_get (model, &iter, PORT_IS_SSL_COLUMN, &is_ssl, -1); + if (!is_ssl && (iters == index)) { + gtk_combo_box_set_active_iter ( + GTK_COMBO_BOX (port_entry), &iter); + return; + } + + if (!is_ssl) + iters++; + + } while (gtk_tree_model_iter_next (model, &iter)); +} diff --git a/e-util/e-port-entry.h b/e-util/e-port-entry.h new file mode 100644 index 0000000000..fc0eaa0d90 --- /dev/null +++ b/e-util/e-port-entry.h @@ -0,0 +1,91 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Authors: + * Dan Vratil <dvratil@redhat.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_PORT_ENTRY_H +#define E_PORT_ENTRY_H + +#include <gtk/gtk.h> +#include <camel/camel.h> + +/* Standard GObject macros */ +#define E_TYPE_PORT_ENTRY \ + (e_port_entry_get_type ()) +#define E_PORT_ENTRY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_PORT_ENTRY, EPortEntry)) +#define E_PORT_ENTRY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_PORT_ENTRY, EPortEntryClass)) +#define E_IS_PORT_ENTRY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_PORT_ENTRY)) +#define E_IS_PORT_ENTRY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_PORT_ENTRY)) +#define E_PORT_ENTRY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_PORT_ENTRY, EPortEntryClass)) + +G_BEGIN_DECLS + +typedef struct _EPortEntry EPortEntry; +typedef struct _EPortEntryClass EPortEntryClass; +typedef struct _EPortEntryPrivate EPortEntryPrivate; + +struct _EPortEntry { + GtkComboBox parent; + EPortEntryPrivate *priv; +}; + +struct _EPortEntryClass { + GtkComboBoxClass parent_class; +}; + +GType e_port_entry_get_type (void) G_GNUC_CONST; +GtkWidget * e_port_entry_new (void); +void e_port_entry_set_camel_entries + (EPortEntry *port_entry, + CamelProviderPortEntry *entries); +gint e_port_entry_get_port (EPortEntry *port_entry); +void e_port_entry_set_port (EPortEntry *port_entry, + gint port); +gboolean e_port_entry_is_valid (EPortEntry *port_entry); +CamelNetworkSecurityMethod + e_port_entry_get_security_method + (EPortEntry *port_entry); +void e_port_entry_set_security_method + (EPortEntry *port_entry, + CamelNetworkSecurityMethod method); +void e_port_entry_activate_secured_port + (EPortEntry *port_entry, + gint index); +void e_port_entry_activate_nonsecured_port + (EPortEntry *port_entry, + gint index); + +G_END_DECLS + +#endif /* E_PORT_ENTRY_H */ diff --git a/e-util/e-preferences-window.c b/e-util/e-preferences-window.c new file mode 100644 index 0000000000..fbe6c01d21 --- /dev/null +++ b/e-util/e-preferences-window.c @@ -0,0 +1,643 @@ +/* + * e-preferences-window.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-preferences-window.h" + +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include "e-misc-utils.h" + +#define SWITCH_PAGE_INTERVAL 250 + +#define E_PREFERENCES_WINDOW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_PREFERENCES_WINDOW, EPreferencesWindowPrivate)) + +struct _EPreferencesWindowPrivate { + gboolean setup; + gpointer shell; + + GtkWidget *icon_view; + GtkWidget *scroll; + GtkWidget *notebook; + GHashTable *index; + + GtkListStore *store; + GtkTreeModelFilter *filter; + const gchar *filter_view; +}; + +enum { + COLUMN_ID, /* G_TYPE_STRING */ + COLUMN_TEXT, /* G_TYPE_STRING */ + COLUMN_HELP, /* G_TYPE_STRING */ + COLUMN_PIXBUF, /* GDK_TYPE_PIXBUF */ + COLUMN_PAGE, /* G_TYPE_INT */ + COLUMN_SORT /* G_TYPE_INT */ +}; + +G_DEFINE_TYPE ( + EPreferencesWindow, + e_preferences_window, + GTK_TYPE_WINDOW) + +static gboolean +preferences_window_filter_view (GtkTreeModel *model, + GtkTreeIter *iter, + EPreferencesWindow *window) +{ + gchar *str; + gboolean visible = FALSE; + + if (!window->priv->filter_view) + return TRUE; + + gtk_tree_model_get (model, iter, COLUMN_ID, &str, -1); + if (strncmp (window->priv->filter_view, "mail", 4) == 0) { + /* Show everything except calendar */ + if (str && (strncmp (str, "cal", 3) == 0)) + visible = FALSE; + else + visible = TRUE; + } else if (strncmp (window->priv->filter_view, "cal", 3) == 0) { + /* Show only calendar and nothing else */ + if (str && (strncmp (str, "cal", 3) != 0)) + visible = FALSE; + else + visible = TRUE; + + } else /* In any other case, show everything */ + visible = TRUE; + + g_free (str); + + return visible; +} + +static GdkPixbuf * +preferences_window_load_pixbuf (const gchar *icon_name) +{ + GtkIconTheme *icon_theme; + GtkIconInfo *icon_info; + GdkPixbuf *pixbuf; + const gchar *filename; + gint size; + GError *error = NULL; + + icon_theme = gtk_icon_theme_get_default (); + + if (!gtk_icon_size_lookup (GTK_ICON_SIZE_DIALOG, &size, 0)) + return NULL; + + icon_info = gtk_icon_theme_lookup_icon ( + icon_theme, icon_name, size, 0); + + if (icon_info == NULL) + return NULL; + + filename = gtk_icon_info_get_filename (icon_info); + + pixbuf = gdk_pixbuf_new_from_file (filename, &error); + + gtk_icon_info_free (icon_info); + + if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } + + return pixbuf; +} + +static void +preferences_window_help_clicked_cb (EPreferencesWindow *window) +{ + GtkTreeModel *model; + GtkTreeIter iter; + GList *list; + gchar *help = NULL; + + g_return_if_fail (window != NULL); + + model = GTK_TREE_MODEL (window->priv->filter); + list = gtk_icon_view_get_selected_items ( + GTK_ICON_VIEW (window->priv->icon_view)); + + if (list != NULL) { + gtk_tree_model_get_iter (model, &iter, list->data); + gtk_tree_model_get (model, &iter, COLUMN_HELP, &help, -1); + + } else if (gtk_tree_model_get_iter_first (model, &iter)) { + gint page_index, current_index; + + current_index = gtk_notebook_get_current_page ( + GTK_NOTEBOOK (window->priv->notebook)); + do { + gtk_tree_model_get ( + model, &iter, COLUMN_PAGE, &page_index, -1); + + if (page_index == current_index) { + gtk_tree_model_get ( + model, &iter, COLUMN_HELP, &help, -1); + break; + } + } while (gtk_tree_model_iter_next (model, &iter)); + } + + e_display_help (GTK_WINDOW (window), help ? help : "index"); + + g_free (help); +} + +static void +preferences_window_selection_changed_cb (EPreferencesWindow *window) +{ + GtkIconView *icon_view; + GtkNotebook *notebook; + GtkTreeModel *model; + GtkTreeIter iter; + GList *list; + gint page; + + icon_view = GTK_ICON_VIEW (window->priv->icon_view); + list = gtk_icon_view_get_selected_items (icon_view); + if (list == NULL) + return; + + model = GTK_TREE_MODEL (window->priv->filter); + gtk_tree_model_get_iter (model, &iter, list->data); + gtk_tree_model_get (model, &iter, COLUMN_PAGE, &page, -1); + + notebook = GTK_NOTEBOOK (window->priv->notebook); + gtk_notebook_set_current_page (notebook, page); + + g_list_foreach (list, (GFunc) gtk_tree_path_free, NULL); + g_list_free (list); + + gtk_widget_grab_focus (GTK_WIDGET (icon_view)); +} + +static void +preferences_window_dispose (GObject *object) +{ + EPreferencesWindowPrivate *priv; + + priv = E_PREFERENCES_WINDOW_GET_PRIVATE (object); + + if (priv->icon_view != NULL) { + g_object_unref (priv->icon_view); + priv->icon_view = NULL; + } + + if (priv->notebook != NULL) { + g_object_unref (priv->notebook); + priv->notebook = NULL; + } + + if (priv->shell) { + g_object_remove_weak_pointer (priv->shell, &priv->shell); + priv->shell = NULL; + } + + g_hash_table_remove_all (priv->index); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_preferences_window_parent_class)->dispose (object); +} + +static void +preferences_window_finalize (GObject *object) +{ + EPreferencesWindowPrivate *priv; + + priv = E_PREFERENCES_WINDOW_GET_PRIVATE (object); + + g_hash_table_destroy (priv->index); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_preferences_window_parent_class)->finalize (object); +} + +static void +preferences_window_show (GtkWidget *widget) +{ + EPreferencesWindowPrivate *priv; + GtkIconView *icon_view; + GtkTreePath *path; + + priv = E_PREFERENCES_WINDOW_GET_PRIVATE (widget); + if (!priv->setup) + g_warning ("Preferences window has not been setup correctly"); + + icon_view = GTK_ICON_VIEW (priv->icon_view); + + path = gtk_tree_path_new_first (); + gtk_icon_view_select_path (icon_view, path); + gtk_icon_view_scroll_to_path (icon_view, path, FALSE, 0.0, 0.0); + gtk_tree_path_free (path); + + gtk_widget_grab_focus (priv->icon_view); + + /* Chain up to parent's show() method. */ + GTK_WIDGET_CLASS (e_preferences_window_parent_class)->show (widget); +} + +static void +e_preferences_window_class_init (EPreferencesWindowClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EPreferencesWindowPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = preferences_window_dispose; + object_class->finalize = preferences_window_finalize; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = preferences_window_show; +} + +static void +e_preferences_window_init (EPreferencesWindow *window) +{ + GtkListStore *store; + GtkWidget *container; + GtkWidget *hbox; + GtkWidget *vbox; + GtkWidget *widget; + GHashTable *index; + const gchar *title; + GtkAccelGroup *accel_group; + + index = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) gtk_tree_row_reference_free); + + window->priv = E_PREFERENCES_WINDOW_GET_PRIVATE (window); + window->priv->index = index; + window->priv->filter_view = NULL; + + store = gtk_list_store_new ( + 6, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, + GDK_TYPE_PIXBUF, G_TYPE_INT, G_TYPE_INT); + gtk_tree_sortable_set_sort_column_id ( + GTK_TREE_SORTABLE (store), COLUMN_SORT, GTK_SORT_ASCENDING); + window->priv->store = store; + + window->priv->filter = (GtkTreeModelFilter *) + gtk_tree_model_filter_new (GTK_TREE_MODEL (store), NULL); + gtk_tree_model_filter_set_visible_func ( + window->priv->filter, (GtkTreeModelFilterVisibleFunc) + preferences_window_filter_view, window, NULL); + + title = _("Evolution Preferences"); + gtk_window_set_title (GTK_WINDOW (window), title); + gtk_window_set_resizable (GTK_WINDOW (window), TRUE); + gtk_container_set_border_width (GTK_CONTAINER (window), 12); + + g_signal_connect ( + window, "delete-event", + G_CALLBACK (gtk_widget_hide_on_delete), NULL); + + widget = gtk_vbox_new (FALSE, 12); + gtk_container_add (GTK_CONTAINER (window), widget); + gtk_widget_show (widget); + + vbox = widget; + + widget = gtk_hbox_new (FALSE, 12); + gtk_box_pack_start (GTK_BOX (vbox), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + hbox = widget; + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, TRUE, 0); + window->priv->scroll = widget; + gtk_widget_show (widget); + + container = widget; + + widget = gtk_icon_view_new_with_model ( + GTK_TREE_MODEL (window->priv->filter)); + gtk_icon_view_set_columns (GTK_ICON_VIEW (widget), 1); + gtk_icon_view_set_text_column (GTK_ICON_VIEW (widget), COLUMN_TEXT); + gtk_icon_view_set_pixbuf_column (GTK_ICON_VIEW (widget), COLUMN_PIXBUF); + g_signal_connect_swapped ( + widget, "selection-changed", + G_CALLBACK (preferences_window_selection_changed_cb), window); + gtk_container_add (GTK_CONTAINER (container), widget); + window->priv->icon_view = g_object_ref (widget); + gtk_widget_show (widget); + g_object_unref (store); + + widget = gtk_notebook_new (); + gtk_notebook_set_show_tabs (GTK_NOTEBOOK (widget), FALSE); + gtk_notebook_set_show_border (GTK_NOTEBOOK (widget), FALSE); + gtk_box_pack_start (GTK_BOX (hbox), widget, TRUE, TRUE, 0); + window->priv->notebook = g_object_ref (widget); + gtk_widget_show (widget); + + widget = gtk_hbutton_box_new (); + gtk_button_box_set_layout ( + GTK_BUTTON_BOX (widget), GTK_BUTTONBOX_END); + gtk_box_pack_start (GTK_BOX (vbox), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_button_new_from_stock (GTK_STOCK_HELP); + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (preferences_window_help_clicked_cb), window); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_button_box_set_child_secondary ( + GTK_BUTTON_BOX (container), widget, TRUE); + gtk_widget_show (widget); + + widget = gtk_button_new_from_stock (GTK_STOCK_CLOSE); + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (gtk_widget_hide), window); + gtk_widget_set_can_default (widget, TRUE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + accel_group = gtk_accel_group_new (); + gtk_widget_add_accelerator ( + widget, "activate", accel_group, + GDK_KEY_Escape, (GdkModifierType) 0, + GTK_ACCEL_VISIBLE); + gtk_window_add_accel_group (GTK_WINDOW (window), accel_group); + gtk_widget_grab_default (widget); + gtk_widget_show (widget); +} + +GtkWidget * +e_preferences_window_new (gpointer shell) +{ + EPreferencesWindow *window; + + window = g_object_new (E_TYPE_PREFERENCES_WINDOW, NULL); + + /* ideally should be an object property */ + window->priv->shell = shell; + if (shell) + g_object_add_weak_pointer (shell, &window->priv->shell); + + return GTK_WIDGET (window); +} + +gpointer +e_preferences_window_get_shell (EPreferencesWindow *window) +{ + g_return_val_if_fail (E_IS_PREFERENCES_WINDOW (window), NULL); + + return window->priv->shell; +} + +void +e_preferences_window_add_page (EPreferencesWindow *window, + const gchar *page_name, + const gchar *icon_name, + const gchar *caption, + const gchar *help_target, + EPreferencesWindowCreatePageFn create_fn, + gint sort_order) +{ + GtkTreeRowReference *reference; + GtkIconView *icon_view; + GtkNotebook *notebook; + GtkTreeModel *model; + GtkTreePath *path; + GHashTable *index; + GdkPixbuf *pixbuf; + GtkTreeIter iter; + GtkWidget *align; + gint page; + + g_return_if_fail (E_IS_PREFERENCES_WINDOW (window)); + g_return_if_fail (create_fn != NULL); + g_return_if_fail (page_name != NULL); + g_return_if_fail (icon_name != NULL); + g_return_if_fail (caption != NULL); + + icon_view = GTK_ICON_VIEW (window->priv->icon_view); + notebook = GTK_NOTEBOOK (window->priv->notebook); + + page = gtk_notebook_get_n_pages (notebook); + model = GTK_TREE_MODEL (window->priv->store); + pixbuf = preferences_window_load_pixbuf (icon_name); + + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + COLUMN_ID, page_name, + COLUMN_TEXT, caption, + COLUMN_HELP, help_target, + COLUMN_PIXBUF, pixbuf, + COLUMN_PAGE, page, + COLUMN_SORT, sort_order, + -1); + + index = window->priv->index; + path = gtk_tree_model_get_path (model, &iter); + reference = gtk_tree_row_reference_new (model, path); + g_hash_table_insert (index, g_strdup (page_name), reference); + gtk_tree_path_free (path); + + align = g_object_new (GTK_TYPE_ALIGNMENT, NULL); + gtk_widget_show (GTK_WIDGET (align)); + g_object_set_data (G_OBJECT (align), "create_fn", create_fn); + gtk_notebook_append_page (notebook, align, NULL); + gtk_container_child_set ( + GTK_CONTAINER (notebook), align, + "tab-fill", FALSE, "tab-expand", FALSE, NULL); + + /* Force GtkIconView to recalculate the text wrap width, + * otherwise we get a really narrow icon list on the left + * side of the preferences window. */ + gtk_icon_view_set_item_width (icon_view, -1); + gtk_widget_queue_resize (GTK_WIDGET (window)); +} + +void +e_preferences_window_show_page (EPreferencesWindow *window, + const gchar *page_name) +{ + GtkTreeRowReference *reference; + GtkIconView *icon_view; + GtkTreePath *path; + + g_return_if_fail (E_IS_PREFERENCES_WINDOW (window)); + g_return_if_fail (page_name != NULL); + g_return_if_fail (window->priv->setup); + + icon_view = GTK_ICON_VIEW (window->priv->icon_view); + reference = g_hash_table_lookup (window->priv->index, page_name); + g_return_if_fail (reference != NULL); + + path = gtk_tree_row_reference_get_path (reference); + gtk_icon_view_select_path (icon_view, path); + gtk_icon_view_scroll_to_path (icon_view, path, FALSE, 0.0, 0.0); + gtk_tree_path_free (path); +} + +void +e_preferences_window_filter_page (EPreferencesWindow *window, + const gchar *page_name) +{ + GtkTreeRowReference *reference; + GtkIconView *icon_view; + GtkTreePath *path; + + g_return_if_fail (E_IS_PREFERENCES_WINDOW (window)); + g_return_if_fail (page_name != NULL); + g_return_if_fail (window->priv->setup); + + icon_view = GTK_ICON_VIEW (window->priv->icon_view); + reference = g_hash_table_lookup (window->priv->index, page_name); + g_return_if_fail (reference != NULL); + + path = gtk_tree_row_reference_get_path (reference); + gtk_icon_view_select_path (icon_view, path); + gtk_icon_view_scroll_to_path (icon_view, path, FALSE, 0.0, 0.0); + gtk_tree_path_free (path); + + window->priv->filter_view = page_name; + gtk_tree_model_filter_refilter (window->priv->filter); + + /* XXX: We need a better solution to hide the icon view when + * there is just one entry */ + if (strncmp (page_name, "cal", 3) == 0) { + gtk_widget_hide (window->priv->scroll); + } else + gtk_widget_show (window->priv->scroll); +} + +/* + * Create all the deferred configuration pages. + */ +void +e_preferences_window_setup (EPreferencesWindow *window) +{ + gint i, num; + GtkNotebook *notebook; + GtkRequisition requisition; + gint width = -1, height = -1, content_width = -1, content_height = -1; + EPreferencesWindowPrivate *priv; + + g_return_if_fail (E_IS_PREFERENCES_WINDOW (window)); + + priv = E_PREFERENCES_WINDOW_GET_PRIVATE (window); + + if (priv->setup) + return; + + gtk_window_get_default_size (GTK_WINDOW (window), &width, &height); + if (width < 0 || height < 0) { + gtk_widget_get_preferred_size (GTK_WIDGET (window), &requisition, NULL); + + width = requisition.width; + height = requisition.height; + } + + notebook = GTK_NOTEBOOK (priv->notebook); + num = gtk_notebook_get_n_pages (notebook); + + for (i = 0; i < num; i++) { + GtkBin *align; + GtkWidget *content; + EPreferencesWindowCreatePageFn create_fn; + + align = GTK_BIN (gtk_notebook_get_nth_page (notebook, i)); + create_fn = g_object_get_data (G_OBJECT (align), "create_fn"); + + if (!create_fn || gtk_bin_get_child (align)) + continue; + + content = create_fn (window); + if (content) { + GtkScrolledWindow *scrolled; + + scrolled = GTK_SCROLLED_WINDOW (gtk_scrolled_window_new (NULL, NULL)); + gtk_scrolled_window_add_with_viewport (scrolled, content); + gtk_scrolled_window_set_min_content_width (scrolled, 320); + gtk_scrolled_window_set_min_content_height (scrolled, 240); + gtk_scrolled_window_set_policy (scrolled, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (scrolled, GTK_SHADOW_NONE); + + gtk_viewport_set_shadow_type ( + GTK_VIEWPORT (gtk_bin_get_child (GTK_BIN (scrolled))), + GTK_SHADOW_NONE); + + gtk_widget_show (content); + + gtk_widget_get_preferred_size (GTK_WIDGET (content), &requisition, NULL); + + if (requisition.width > content_width) + content_width = requisition.width; + if (requisition.height > content_height) + content_height = requisition.height; + + gtk_widget_show (GTK_WIDGET (scrolled)); + + gtk_container_add (GTK_CONTAINER (align), GTK_WIDGET (scrolled)); + } + } + + if (content_width > 0 && content_height > 0 && width > 0 && height > 0) { + GdkScreen *screen; + GdkRectangle monitor_area; + gint x = 0, y = 0, monitor; + + screen = gtk_window_get_screen (GTK_WINDOW (window)); + gtk_window_get_position (GTK_WINDOW (window), &x, &y); + + monitor = gdk_screen_get_monitor_at_point (screen, x, y); + if (monitor < 0 || monitor >= gdk_screen_get_n_monitors (screen)) + monitor = 0; + + gdk_screen_get_monitor_workarea (screen, monitor, &monitor_area); + + if (content_width > monitor_area.width - width) + content_width = monitor_area.width - width; + + if (content_height > monitor_area.height - height) + content_height = monitor_area.height - height; + + if (content_width > 0 && content_height > 0) + gtk_window_set_default_size (GTK_WINDOW (window), width + content_width, height + content_height); + } + + priv->setup = TRUE; +} diff --git a/e-util/e-preferences-window.h b/e-util/e-preferences-window.h new file mode 100644 index 0000000000..f2efa015e6 --- /dev/null +++ b/e-util/e-preferences-window.h @@ -0,0 +1,88 @@ +/* + * e-preferences-window.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_PREFERENCES_WINDOW_H +#define E_PREFERENCES_WINDOW_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_PREFERENCES_WINDOW \ + (e_preferences_window_get_type ()) +#define E_PREFERENCES_WINDOW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_PREFERENCES_WINDOW, EPreferencesWindow)) +#define E_PREFERENCES_WINDOW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_PREFERENCES_WINDOW, EPreferencesWindowClass)) +#define E_IS_PREFERENCES_WINDOW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_PREFERENCES_WINDOW)) +#define E_IS_PREFERENCES_WINDOW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((obj), E_TYPE_PREFERENCES_WINDOW)) +#define E_PREFERENCES_WINDOW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_TYPE \ + ((obj), E_TYPE_PREFERENCES_WINDOW, EPreferencesWindowClass)) + +G_BEGIN_DECLS + +typedef struct _EPreferencesWindow EPreferencesWindow; +typedef struct _EPreferencesWindowClass EPreferencesWindowClass; +typedef struct _EPreferencesWindowPrivate EPreferencesWindowPrivate; + +struct _EPreferencesWindow { + GtkWindow parent; + EPreferencesWindowPrivate *priv; +}; + +struct _EPreferencesWindowClass { + GtkWindowClass parent_class; +}; + +typedef GtkWidget * + (*EPreferencesWindowCreatePageFn) + (EPreferencesWindow *window); + +GType e_preferences_window_get_type (void); +GtkWidget * e_preferences_window_new (gpointer shell); +gpointer e_preferences_window_get_shell (EPreferencesWindow *window); +void e_preferences_window_setup (EPreferencesWindow *window); +void e_preferences_window_add_page (EPreferencesWindow *window, + const gchar *page_name, + const gchar *icon_name, + const gchar *caption, + const gchar *help_target, + EPreferencesWindowCreatePageFn create_fn, + gint sort_order); +void e_preferences_window_show_page (EPreferencesWindow *window, + const gchar *page_name); +void e_preferences_window_filter_page + (EPreferencesWindow *window, + const gchar *page_name); + +G_END_DECLS + +#endif /* E_PREFERENCES_WINDOW_H */ diff --git a/e-util/e-preview-pane.c b/e-util/e-preview-pane.c new file mode 100644 index 0000000000..27009ab087 --- /dev/null +++ b/e-util/e-preview-pane.c @@ -0,0 +1,322 @@ +/* + * e-preview-pane.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-preview-pane.h" + +#include <gdk/gdkkeysyms.h> + +#include "e-alert-bar.h" +#include "e-alert-dialog.h" +#include "e-alert-sink.h" + +#define E_PREVIEW_PANE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_PREVIEW_PANE, EPreviewPanePrivate)) + +struct _EPreviewPanePrivate { + GtkWidget *alert_bar; + GtkWidget *web_view; + GtkWidget *search_bar; +}; + +enum { + PROP_0, + PROP_SEARCH_BAR, + PROP_WEB_VIEW +}; + +enum { + SHOW_SEARCH_BAR, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +/* Forward Declarations */ +static void e_preview_pane_alert_sink_init + (EAlertSinkInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EPreviewPane, + e_preview_pane, + GTK_TYPE_VBOX, + G_IMPLEMENT_INTERFACE ( + E_TYPE_ALERT_SINK, + e_preview_pane_alert_sink_init)) + +static void +preview_pane_set_web_view (EPreviewPane *preview_pane, + EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + g_return_if_fail (preview_pane->priv->web_view == NULL); + + preview_pane->priv->web_view = g_object_ref_sink (web_view); +} + +static void +preview_pane_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_WEB_VIEW: + preview_pane_set_web_view ( + E_PREVIEW_PANE (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +preview_pane_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SEARCH_BAR: + g_value_set_object ( + value, e_preview_pane_get_search_bar ( + E_PREVIEW_PANE (object))); + return; + + case PROP_WEB_VIEW: + g_value_set_object ( + value, e_preview_pane_get_web_view ( + E_PREVIEW_PANE (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +preview_pane_dispose (GObject *object) +{ + EPreviewPanePrivate *priv; + + priv = E_PREVIEW_PANE_GET_PRIVATE (object); + + if (priv->alert_bar != NULL) { + g_object_unref (priv->alert_bar); + priv->alert_bar = NULL; + } + + if (priv->search_bar != NULL) { + g_object_unref (priv->search_bar); + priv->search_bar = NULL; + } + + if (priv->web_view != NULL) { + g_object_unref (priv->web_view); + priv->web_view = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_preview_pane_parent_class)->dispose (object); +} + +static void +preview_pane_constructed (GObject *object) +{ + EPreviewPanePrivate *priv; + GtkWidget *widget; + + priv = E_PREVIEW_PANE_GET_PRIVATE (object); + + widget = e_alert_bar_new (); + gtk_box_pack_start (GTK_BOX (object), widget, FALSE, FALSE, 0); + priv->alert_bar = g_object_ref (widget); + /* EAlertBar controls its own visibility. */ + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (object), widget, TRUE, TRUE, 0); + gtk_container_add (GTK_CONTAINER (widget), priv->web_view); + gtk_widget_show (widget); + gtk_widget_show (priv->web_view); + + widget = e_search_bar_new (E_WEB_VIEW (priv->web_view)); + gtk_box_pack_start (GTK_BOX (object), widget, FALSE, FALSE, 0); + priv->search_bar = g_object_ref (widget); + gtk_widget_hide (widget); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_preview_pane_parent_class)->constructed (object); +} + +static void +preview_pane_show_search_bar (EPreviewPane *preview_pane) +{ + GtkWidget *search_bar; + + search_bar = preview_pane->priv->search_bar; + + if (!gtk_widget_get_visible (search_bar)) + gtk_widget_show (search_bar); +} + +static void +preview_pane_submit_alert (EAlertSink *alert_sink, + EAlert *alert) +{ + EPreviewPane *preview_pane; + EAlertBar *alert_bar; + GtkWidget *dialog; + GtkWindow *parent; + + preview_pane = E_PREVIEW_PANE (alert_sink); + alert_bar = E_ALERT_BAR (preview_pane->priv->alert_bar); + + switch (e_alert_get_message_type (alert)) { + case GTK_MESSAGE_INFO: + case GTK_MESSAGE_WARNING: + case GTK_MESSAGE_QUESTION: + case GTK_MESSAGE_ERROR: + e_alert_bar_add_alert (alert_bar, alert); + break; + + default: + parent = GTK_WINDOW (alert_sink); + dialog = e_alert_dialog_new (parent, alert); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + break; + } +} + +static void +e_preview_pane_class_init (EPreviewPaneClass *class) +{ + GObjectClass *object_class; + GtkBindingSet *binding_set; + + g_type_class_add_private (class, sizeof (EPreviewPanePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = preview_pane_set_property; + object_class->get_property = preview_pane_get_property; + object_class->dispose = preview_pane_dispose; + object_class->constructed = preview_pane_constructed; + + class->show_search_bar = preview_pane_show_search_bar; + + g_object_class_install_property ( + object_class, + PROP_SEARCH_BAR, + g_param_spec_object ( + "search-bar", + "Search Bar", + NULL, + E_TYPE_SEARCH_BAR, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_WEB_VIEW, + g_param_spec_object ( + "web-view", + "Web View", + NULL, + E_TYPE_WEB_VIEW, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); + + signals[SHOW_SEARCH_BAR] = g_signal_new ( + "show-search-bar", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EPreviewPaneClass, show_search_bar), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + binding_set = gtk_binding_set_by_class (class); + + gtk_binding_entry_add_signal ( + binding_set, GDK_KEY_f, GDK_SHIFT_MASK | GDK_CONTROL_MASK, + "show-search-bar", 0); +} + +static void +e_preview_pane_alert_sink_init (EAlertSinkInterface *interface) +{ + interface->submit_alert = preview_pane_submit_alert; +} + +static void +e_preview_pane_init (EPreviewPane *preview_pane) +{ + preview_pane->priv = E_PREVIEW_PANE_GET_PRIVATE (preview_pane); + + gtk_box_set_spacing (GTK_BOX (preview_pane), 1); +} + +GtkWidget * +e_preview_pane_new (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return g_object_new ( + E_TYPE_PREVIEW_PANE, + "web-view", web_view, NULL); +} + +EWebView * +e_preview_pane_get_web_view (EPreviewPane *preview_pane) +{ + g_return_val_if_fail (E_IS_PREVIEW_PANE (preview_pane), NULL); + + return E_WEB_VIEW (preview_pane->priv->web_view); +} + +ESearchBar * +e_preview_pane_get_search_bar (EPreviewPane *preview_pane) +{ + g_return_val_if_fail (E_IS_PREVIEW_PANE (preview_pane), NULL); + + return E_SEARCH_BAR (preview_pane->priv->search_bar); +} + +void +e_preview_pane_clear_alerts (EPreviewPane *preview_pane) +{ + g_return_if_fail (E_IS_PREVIEW_PANE (preview_pane)); + + e_alert_bar_clear (E_ALERT_BAR (preview_pane->priv->alert_bar)); +} + +void +e_preview_pane_show_search_bar (EPreviewPane *preview_pane) +{ + g_return_if_fail (E_IS_PREVIEW_PANE (preview_pane)); + + g_signal_emit (preview_pane, signals[SHOW_SEARCH_BAR], 0); +} diff --git a/e-util/e-preview-pane.h b/e-util/e-preview-pane.h new file mode 100644 index 0000000000..3720744b6c --- /dev/null +++ b/e-util/e-preview-pane.h @@ -0,0 +1,80 @@ +/* + * e-preview-pane.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_PREVIEW_PANE_H +#define E_PREVIEW_PANE_H + +#include <gtk/gtk.h> + +#include <e-util/e-search-bar.h> +#include <e-util/e-web-view.h> + +/* Standard GObject macros */ +#define E_TYPE_PREVIEW_PANE \ + (e_preview_pane_get_type ()) +#define E_PREVIEW_PANE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_PREVIEW_PANE, EPreviewPane)) +#define E_PREVIEW_PANE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_PREVIEW_PANE, EPreviewPaneClass)) +#define E_IS_PREVIEW_PANE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_PREVIEW_PANE)) +#define E_IS_PREVIEW_PANE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_PREVIEW_PANE)) +#define E_PREVIEW_PANE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_PREVIEW_PANE, EPreviewPaneClass)) + +G_BEGIN_DECLS + +typedef struct _EPreviewPane EPreviewPane; +typedef struct _EPreviewPaneClass EPreviewPaneClass; +typedef struct _EPreviewPanePrivate EPreviewPanePrivate; + +struct _EPreviewPane { + GtkBox parent; + EPreviewPanePrivate *priv; +}; + +struct _EPreviewPaneClass { + GtkBoxClass parent_class; + + /* Signals */ + void (*show_search_bar) (EPreviewPane *preview_pane); +}; + +GType e_preview_pane_get_type (void); +GtkWidget * e_preview_pane_new (EWebView *web_view); +EWebView * e_preview_pane_get_web_view (EPreviewPane *preview_pane); +ESearchBar * e_preview_pane_get_search_bar (EPreviewPane *preview_pane); +void e_preview_pane_clear_alerts (EPreviewPane *preview_pane); +void e_preview_pane_show_search_bar (EPreviewPane *preview_pane); + +G_END_DECLS + +#endif /* E_PREVIEW_PANE_H */ diff --git a/e-util/e-print.c b/e-util/e-print.c index e50ffd8647..bc2f4d3608 100644 --- a/e-util/e-print.c +++ b/e-util/e-print.c @@ -32,7 +32,7 @@ #include <gtk/gtk.h> #include <glib/gi18n.h> -#include "e-util.h" +#include <libedataserver/libedataserver.h> /* XXX Would be better if GtkPrint exposed these. */ #define PAGE_SETUP_GROUP_NAME "Page Setup" diff --git a/e-util/e-print.h b/e-util/e-print.h index 9469b63636..ee96e25d2e 100644 --- a/e-util/e-print.h +++ b/e-util/e-print.h @@ -23,6 +23,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef __E_PRINT__ #define __E_PRINT__ diff --git a/e-util/e-printable.c b/e-util/e-printable.c new file mode 100644 index 0000000000..3f1403f03c --- /dev/null +++ b/e-util/e-printable.c @@ -0,0 +1,226 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> + +#include "e-printable.h" + +#include "e-marshal.h" + +#define EP_CLASS(e) ((EPrintableClass *)((GObject *)e)->class) + +G_DEFINE_TYPE ( + EPrintable, + e_printable, + G_TYPE_INITIALLY_UNOWNED) + +enum { + PRINT_PAGE, + DATA_LEFT, + RESET, + HEIGHT, + WILL_FIT, + LAST_SIGNAL +}; + +static guint e_printable_signals[LAST_SIGNAL] = { 0, }; + +static void +e_printable_class_init (EPrintableClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + e_printable_signals[PRINT_PAGE] = g_signal_new ( + "print_page", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EPrintableClass, print_page), + NULL, NULL, + e_marshal_NONE__OBJECT_DOUBLE_DOUBLE_BOOLEAN, + G_TYPE_NONE, 4, + G_TYPE_OBJECT, + G_TYPE_DOUBLE, + G_TYPE_DOUBLE, + G_TYPE_BOOLEAN); + + e_printable_signals[DATA_LEFT] = g_signal_new ( + "data_left", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EPrintableClass, data_left), + NULL, NULL, + e_marshal_BOOLEAN__NONE, + G_TYPE_BOOLEAN, 0, + G_TYPE_NONE); + + e_printable_signals[RESET] = g_signal_new ( + "reset", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EPrintableClass, reset), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0, + G_TYPE_NONE); + + e_printable_signals[HEIGHT] = g_signal_new ( + "height", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EPrintableClass, height), + NULL, NULL, + e_marshal_DOUBLE__OBJECT_DOUBLE_DOUBLE_BOOLEAN, + G_TYPE_DOUBLE, 4, + G_TYPE_OBJECT, + G_TYPE_DOUBLE, + G_TYPE_DOUBLE, + G_TYPE_BOOLEAN); + + e_printable_signals[WILL_FIT] = g_signal_new ( + "will_fit", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EPrintableClass, will_fit), + NULL, NULL, + e_marshal_BOOLEAN__OBJECT_DOUBLE_DOUBLE_BOOLEAN, + G_TYPE_BOOLEAN, 4, + G_TYPE_OBJECT, + G_TYPE_DOUBLE, + G_TYPE_DOUBLE, + G_TYPE_BOOLEAN); + + class->print_page = NULL; + class->data_left = NULL; + class->reset = NULL; + class->height = NULL; + class->will_fit = NULL; +} + +static void +e_printable_init (EPrintable *e_printable) +{ + /* nothing to do */ +} + +EPrintable * +e_printable_new (void) +{ + return E_PRINTABLE (g_object_new (E_PRINTABLE_TYPE, NULL)); +} + +void +e_printable_print_page (EPrintable *e_printable, + GtkPrintContext *context, + gdouble width, + gdouble height, + gboolean quantized) +{ + g_return_if_fail (e_printable != NULL); + g_return_if_fail (E_IS_PRINTABLE (e_printable)); + + g_signal_emit ( + e_printable, + e_printable_signals[PRINT_PAGE], 0, + context, + width, + height, + quantized); +} + +gboolean +e_printable_data_left (EPrintable *e_printable) +{ + gboolean ret_val; + + g_return_val_if_fail (e_printable != NULL, FALSE); + g_return_val_if_fail (E_IS_PRINTABLE (e_printable), FALSE); + + g_signal_emit ( + e_printable, + e_printable_signals[DATA_LEFT], 0, + &ret_val); + + return ret_val; +} + +void +e_printable_reset (EPrintable *e_printable) +{ + g_return_if_fail (e_printable != NULL); + g_return_if_fail (E_IS_PRINTABLE (e_printable)); + + g_signal_emit ( + e_printable, + e_printable_signals[RESET], 0); +} + +gdouble +e_printable_height (EPrintable *e_printable, + GtkPrintContext *context, + gdouble width, + gdouble max_height, + gboolean quantized) +{ + gdouble ret_val; + + g_return_val_if_fail (e_printable != NULL, -1); + g_return_val_if_fail (E_IS_PRINTABLE (e_printable), -1); + + g_signal_emit ( + e_printable, + e_printable_signals[HEIGHT], 0, + context, + width, + max_height, + quantized, + &ret_val); + + return ret_val; +} + +gboolean +e_printable_will_fit (EPrintable *e_printable, + GtkPrintContext *context, + gdouble width, + gdouble max_height, + gboolean quantized) +{ + gboolean ret_val; + + g_return_val_if_fail (e_printable != NULL, FALSE); + g_return_val_if_fail (E_IS_PRINTABLE (e_printable), FALSE); + + g_signal_emit ( + e_printable, + e_printable_signals[WILL_FIT], 0, + context, + width, + max_height, + quantized, + &ret_val); + + return ret_val; +} diff --git a/e-util/e-printable.h b/e-util/e-printable.h new file mode 100644 index 0000000000..292756076d --- /dev/null +++ b/e-util/e-printable.h @@ -0,0 +1,93 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_PRINTABLE_H_ +#define _E_PRINTABLE_H_ + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define E_PRINTABLE_TYPE (e_printable_get_type ()) +#define E_PRINTABLE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), E_PRINTABLE_TYPE, EPrintable)) +#define E_PRINTABLE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), E_PRINTABLE_TYPE, EPrintableClass)) +#define E_IS_PRINTABLE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), E_PRINTABLE_TYPE)) +#define E_IS_PRINTABLE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), E_PRINTABLE_TYPE)) + +typedef struct { + GObject base; +} EPrintable; + +typedef struct { + GObjectClass parent_class; + + /* + * Signals + */ + + void (*print_page) (EPrintable *etm, GtkPrintContext *context, gdouble width, gdouble height, gboolean quantized); + gboolean (*data_left) (EPrintable *etm); + void (*reset) (EPrintable *etm); + gdouble (*height) (EPrintable *etm, GtkPrintContext *context, gdouble width, gdouble max_height, gboolean quantized); + + /* e_printable_will_fit (ep, ...) should be equal in value to + * (e_printable_print_page (ep, ...), + * !e_printable_data_left(ep)) except that the latter has the + * side effect of doing the printing and advancing the + * position of the printable. + */ + + gboolean (*will_fit) (EPrintable *etm, GtkPrintContext *context, gdouble width, gdouble max_height, gboolean quantized); +} EPrintableClass; + +GType e_printable_get_type (void); + +EPrintable *e_printable_new (void); + +/* + * Routines for emitting signals on the e_table */ +void e_printable_print_page (EPrintable *e_printable, + GtkPrintContext *context, + gdouble width, + gdouble height, + gboolean quantized); +gboolean e_printable_data_left (EPrintable *e_printable); +void e_printable_reset (EPrintable *e_printable); +gdouble e_printable_height (EPrintable *e_printable, + GtkPrintContext *context, + gdouble width, + gdouble max_height, + gboolean quantized); +gboolean e_printable_will_fit (EPrintable *e_printable, + GtkPrintContext *context, + gdouble width, + gdouble max_height, + gboolean quantized); + +G_END_DECLS + +#endif /* _E_PRINTABLE_H_ */ diff --git a/e-util/e-reflow-model.c b/e-util/e-reflow-model.c new file mode 100644 index 0000000000..4f5b219b90 --- /dev/null +++ b/e-util/e-reflow-model.c @@ -0,0 +1,411 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-reflow-model.h" + +#include "e-misc-utils.h" + +G_DEFINE_TYPE (EReflowModel, e_reflow_model, G_TYPE_OBJECT) + +#define d(x) + +d (static gint depth = 0;) + +enum { + MODEL_CHANGED, + COMPARISON_CHANGED, + MODEL_ITEMS_INSERTED, + MODEL_ITEM_CHANGED, + MODEL_ITEM_REMOVED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +/** + * e_reflow_model_set_width: + * @e_reflow_model: The e-reflow-model to operate on + * @width: The new value for the width of each item. + */ +void +e_reflow_model_set_width (EReflowModel *e_reflow_model, + gint width) +{ + EReflowModelClass *class; + + g_return_if_fail (E_IS_REFLOW_MODEL (e_reflow_model)); + + class = E_REFLOW_MODEL_GET_CLASS (e_reflow_model); + g_return_if_fail (class->set_width != NULL); + + class->set_width (e_reflow_model, width); +} + +/** + * e_reflow_model_count: + * @e_reflow_model: The e-reflow-model to operate on + * + * Returns: the number of items in the reflow model. + */ +gint +e_reflow_model_count (EReflowModel *e_reflow_model) +{ + EReflowModelClass *class; + + g_return_val_if_fail (E_IS_REFLOW_MODEL (e_reflow_model), 0); + + class = E_REFLOW_MODEL_GET_CLASS (e_reflow_model); + g_return_val_if_fail (class->count != NULL, 0); + + return class->count (e_reflow_model); +} + +/** + * e_reflow_model_height: + * @e_reflow_model: The e-reflow-model to operate on + * @n: The item number to get the height of. + * @parent: The parent GnomeCanvasItem. + * + * Returns: the height of the nth item. + */ +gint +e_reflow_model_height (EReflowModel *e_reflow_model, + gint n, + GnomeCanvasGroup *parent) +{ + EReflowModelClass *class; + + g_return_val_if_fail (E_IS_REFLOW_MODEL (e_reflow_model), 0); + + class = E_REFLOW_MODEL_GET_CLASS (e_reflow_model); + g_return_val_if_fail (class->height != NULL, 0); + + return class->height (e_reflow_model, n, parent); +} + +/** + * e_reflow_model_incarnate: + * @e_reflow_model: The e-reflow-model to operate on + * @n: The item to create. + * @parent: The parent GnomeCanvasItem to create a child of. + * + * Create a GnomeCanvasItem to represent the nth piece of data. + * + * Returns: the new GnomeCanvasItem. + */ +GnomeCanvasItem * +e_reflow_model_incarnate (EReflowModel *e_reflow_model, + gint n, + GnomeCanvasGroup *parent) +{ + EReflowModelClass *class; + + g_return_val_if_fail (E_IS_REFLOW_MODEL (e_reflow_model), NULL); + + class = E_REFLOW_MODEL_GET_CLASS (e_reflow_model); + g_return_val_if_fail (class->incarnate != NULL, NULL); + + return class->incarnate (e_reflow_model, n, parent); +} + +/** + * e_reflow_model_create_cmp_cache: + * @e_reflow_model: The e-reflow-model to operate on + * + * Creates a compare cache for quicker sorting. The sorting function + * may not depend on the cache, but it should benefit from it if available. + * + * Returns: Newly created GHashTable with cached compare values. This will be + * automatically freed with g_hash_table_destroy() when no longer needed. + **/ +GHashTable * +e_reflow_model_create_cmp_cache (EReflowModel *e_reflow_model) +{ + EReflowModelClass *class; + + g_return_val_if_fail (E_IS_REFLOW_MODEL (e_reflow_model), NULL); + + class = E_REFLOW_MODEL_GET_CLASS (e_reflow_model); + + if (class->create_cmp_cache == NULL) + return NULL; + + return class->create_cmp_cache (e_reflow_model); +} + +/** + * e_reflow_model_compare: + * @e_reflow_model: The e-reflow-model to operate on + * @n1: The first item to compare + * @n2: The second item to compare + * @cmp_cache: #GHashTable of cached compare values, created by + * e_reflow_model_create_cmp_cache(). This can be NULL, when + * caching is not available, even when @e_reflow_model defines + * the create_cmp_cache function. + * + * Compares item n1 and item n2 to see which should come first. + * + * Returns: strcmp like semantics for the comparison value. + */ +gint +e_reflow_model_compare (EReflowModel *e_reflow_model, + gint n1, + gint n2, + GHashTable *cmp_cache) +{ + EReflowModelClass *class; + + g_return_val_if_fail (E_IS_REFLOW_MODEL (e_reflow_model), 0); + + class = E_REFLOW_MODEL_GET_CLASS (e_reflow_model); + g_return_val_if_fail (class->compare != NULL, 0); + + return class->compare (e_reflow_model, n1, n2, cmp_cache); +} + +/** + * e_reflow_model_reincarnate: + * @e_reflow_model: The e-reflow-model to operate on + * @n: The item to create. + * @item: The item to reuse. + * + * Update item to represent the nth piece of data. + */ +void +e_reflow_model_reincarnate (EReflowModel *e_reflow_model, + gint n, + GnomeCanvasItem *item) +{ + EReflowModelClass *class; + + g_return_if_fail (E_IS_REFLOW_MODEL (e_reflow_model)); + + class = E_REFLOW_MODEL_GET_CLASS (e_reflow_model); + g_return_if_fail (class->reincarnate != NULL); + + class->reincarnate (e_reflow_model, n, item); +} + +static void +e_reflow_model_class_init (EReflowModelClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + class->set_width = NULL; + class->count = NULL; + class->height = NULL; + class->incarnate = NULL; + class->reincarnate = NULL; + + class->model_changed = NULL; + class->comparison_changed = NULL; + class->model_items_inserted = NULL; + class->model_item_removed = NULL; + class->model_item_changed = NULL; + + signals[MODEL_CHANGED] = g_signal_new ( + "model_changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EReflowModelClass, model_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[COMPARISON_CHANGED] = g_signal_new ( + "comparison_changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EReflowModelClass, comparison_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[MODEL_ITEMS_INSERTED] = g_signal_new ( + "model_items_inserted", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EReflowModelClass, model_items_inserted), + NULL, NULL, + e_marshal_NONE__INT_INT, + G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_INT); + + signals[MODEL_ITEM_CHANGED] = g_signal_new ( + "model_item_changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EReflowModelClass, model_item_changed), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, G_TYPE_INT); + + signals[MODEL_ITEM_REMOVED] = g_signal_new ( + "model_item_removed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EReflowModelClass, model_item_removed), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, G_TYPE_INT); +} + +static void +e_reflow_model_init (EReflowModel *e_reflow_model) +{ +} + +#if d(!)0 +static void +print_tabs (void) +{ + gint i; + for (i = 0; i < depth; i++) + g_print ("\t"); +} +#endif + +/** + * e_reflow_model_changed: + * @e_reflow_model: the reflow model to notify of the change + * + * Use this function to notify any views of this reflow model that + * the contents of the reflow model have changed. This will emit + * the signal "model_changed" on the @e_reflow_model object. + * + * It is preferable to use the e_reflow_model_item_changed() signal to + * notify of smaller changes than to invalidate the entire model, as + * the views might have ways of caching the information they render + * from the model. + */ +void +e_reflow_model_changed (EReflowModel *e_reflow_model) +{ + g_return_if_fail (e_reflow_model != NULL); + g_return_if_fail (E_IS_REFLOW_MODEL (e_reflow_model)); + + d (print_tabs ()); + d (g_print ("Emitting model_changed on model 0x%p.\n", e_reflow_model)); + d (depth++); + g_signal_emit (e_reflow_model, signals[MODEL_CHANGED], 0); + d (depth--); +} + +/** + * e_reflow_model_comparison_changed: + * @e_reflow_model: the reflow model to notify of the change + * + * Use this function to notify any views of this reflow model that the + * sorting has changed. The actual contents of the items hasn't, so + * there's no need to re-query the model for the heights of the + * individual items. + */ +void +e_reflow_model_comparison_changed (EReflowModel *e_reflow_model) +{ + g_return_if_fail (e_reflow_model != NULL); + g_return_if_fail (E_IS_REFLOW_MODEL (e_reflow_model)); + + d (print_tabs ()); + d (g_print ( + "Emitting comparison_changed on model 0x%p.\n", + e_reflow_model)); + d (depth++); + g_signal_emit (e_reflow_model, signals[COMPARISON_CHANGED], 0); + d (depth--); +} + +/** + * e_reflow_model_items_inserted: + * @e_reflow_model: The model changed. + * @position: The position the items were insert in. + * @count: The number of items inserted. + * + * Use this function to notify any views of the reflow model that a number + * of items have been inserted. + **/ +void +e_reflow_model_items_inserted (EReflowModel *e_reflow_model, + gint position, + gint count) +{ + g_return_if_fail (e_reflow_model != NULL); + g_return_if_fail (E_IS_REFLOW_MODEL (e_reflow_model)); + + d (print_tabs ()); + d (depth++); + g_signal_emit ( + e_reflow_model, + signals[MODEL_ITEMS_INSERTED], 0, + position, count); + d (depth--); +} + +/** + * e_reflow_model_item_removed: + * @e_reflow_model: The model changed. + * @n: The position from which the items were removed. + * + * Use this function to notify any views of the reflow model that an + * item has been removed. + **/ +void +e_reflow_model_item_removed (EReflowModel *e_reflow_model, + gint n) +{ + g_return_if_fail (e_reflow_model != NULL); + g_return_if_fail (E_IS_REFLOW_MODEL (e_reflow_model)); + + d (print_tabs ()); + d (depth++); + g_signal_emit (e_reflow_model, signals[MODEL_ITEM_REMOVED], 0, n); + d (depth--); +} + +/** + * e_reflow_model_item_changed: + * @e_reflow_model: the reflow model to notify of the change + * @item: the item that was changed in the model. + * + * Use this function to notify any views of the reflow model that the + * contents of item @item have changed in model such that the height + * has changed or the item needs to be reincarnated. This function + * will emit the "model_item_changed" signal on the @e_reflow_model + * object + */ +void +e_reflow_model_item_changed (EReflowModel *e_reflow_model, + gint n) +{ + g_return_if_fail (e_reflow_model != NULL); + g_return_if_fail (E_IS_REFLOW_MODEL (e_reflow_model)); + + d (print_tabs ()); + d (g_print ("Emitting item_changed on model 0x%p, n=%d.\n", e_reflow_model, n)); + d (depth++); + g_signal_emit (e_reflow_model, signals[MODEL_ITEM_CHANGED], 0, n); + d (depth--); +} diff --git a/e-util/e-reflow-model.h b/e-util/e-reflow-model.h new file mode 100644 index 0000000000..4a5f710084 --- /dev/null +++ b/e-util/e-reflow-model.h @@ -0,0 +1,129 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_REFLOW_MODEL_H_ +#define _E_REFLOW_MODEL_H_ + +#include <libgnomecanvas/libgnomecanvas.h> + +/* Standard GObject macros */ +#define E_TYPE_REFLOW_MODEL \ + (e_reflow_model_get_type ()) +#define E_REFLOW_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_REFLOW_MODEL, EReflowModel)) +#define E_REFLOW_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_REFLOW_MODEL, EReflowModelClass)) +#define E_IS_REFLOW_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_REFLOW_MODEL)) +#define E_IS_REFLOW_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_REFLOW_MODEL)) +#define E_REFLOW_MODEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_REFLOW_MODEL, EReflowModelClass)) + +G_BEGIN_DECLS + +typedef struct _EReflowModel EReflowModel; +typedef struct _EReflowModelClass EReflowModelClass; + +struct _EReflowModel { + GObject parent; +}; + +struct _EReflowModelClass { + GObjectClass parent_class; + + /* + * Virtual methods + */ + void (*set_width) (EReflowModel *etm, gint width); + + gint (*count) (EReflowModel *etm); + gint (*height) (EReflowModel *etm, gint n, GnomeCanvasGroup *parent); + GnomeCanvasItem *(*incarnate) (EReflowModel *etm, gint n, GnomeCanvasGroup *parent); + GHashTable * (*create_cmp_cache) (EReflowModel *etm); + gint (*compare) (EReflowModel *etm, gint n1, gint n2, GHashTable *cmp_cache); + void (*reincarnate) (EReflowModel *etm, gint n, GnomeCanvasItem *item); + + /* + * Signals + */ + + /* + * These all come after the change has been made. + * Major structural changes: model_changed + * Changes to the sorting of elements: comparison_changed + * Changes only in an item: item_changed + */ + void (*model_changed) (EReflowModel *etm); + void (*comparison_changed) (EReflowModel *etm); + void (*model_items_inserted) (EReflowModel *etm, gint position, gint count); + void (*model_item_removed) (EReflowModel *etm, gint position); + void (*model_item_changed) (EReflowModel *etm, gint n); +}; + +GType e_reflow_model_get_type (void); + +/**/ +void e_reflow_model_set_width (EReflowModel *e_reflow_model, + gint width); +gint e_reflow_model_count (EReflowModel *e_reflow_model); +gint e_reflow_model_height (EReflowModel *e_reflow_model, + gint n, + GnomeCanvasGroup *parent); +GnomeCanvasItem *e_reflow_model_incarnate (EReflowModel *e_reflow_model, + gint n, + GnomeCanvasGroup *parent); +GHashTable * e_reflow_model_create_cmp_cache (EReflowModel *e_reflow_model); +gint e_reflow_model_compare (EReflowModel *e_reflow_model, + gint n1, + gint n2, + GHashTable *cmp_cache); +void e_reflow_model_reincarnate (EReflowModel *e_reflow_model, + gint n, + GnomeCanvasItem *item); + +/* + * Routines for emitting signals on the e_reflow + */ +void e_reflow_model_changed (EReflowModel *e_reflow_model); +void e_reflow_model_comparison_changed (EReflowModel *e_reflow_model); +void e_reflow_model_items_inserted (EReflowModel *e_reflow_model, + gint position, + gint count); +void e_reflow_model_item_removed (EReflowModel *e_reflow_model, + gint n); +void e_reflow_model_item_changed (EReflowModel *e_reflow_model, + gint n); + +G_END_DECLS + +#endif /* _E_REFLOW_MODEL_H_ */ diff --git a/e-util/e-reflow.c b/e-util/e-reflow.c new file mode 100644 index 0000000000..0df0aad5f8 --- /dev/null +++ b/e-util/e-reflow.c @@ -0,0 +1,1732 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-reflow.h" + +#include <math.h> +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include "e-canvas-utils.h" +#include "e-canvas.h" +#include "e-marshal.h" +#include "e-selection-model-simple.h" +#include "e-text.h" +#include "e-unicode.h" + +static gboolean e_reflow_event (GnomeCanvasItem *item, GdkEvent *event); +static void e_reflow_realize (GnomeCanvasItem *item); +static void e_reflow_unrealize (GnomeCanvasItem *item); +static void e_reflow_draw (GnomeCanvasItem *item, cairo_t *cr, + gint x, gint y, gint width, gint height); +static void e_reflow_update (GnomeCanvasItem *item, const cairo_matrix_t *i2c, gint flags); +static GnomeCanvasItem *e_reflow_point (GnomeCanvasItem *item, gdouble x, gdouble y, gint cx, gint cy); +static void e_reflow_reflow (GnomeCanvasItem *item, gint flags); +static void set_empty (EReflow *reflow); + +static void e_reflow_resize_children (GnomeCanvasItem *item); + +#define E_REFLOW_DIVIDER_WIDTH 2 +#define E_REFLOW_BORDER_WIDTH 7 +#define E_REFLOW_FULL_GUTTER (E_REFLOW_DIVIDER_WIDTH + E_REFLOW_BORDER_WIDTH * 2) + +G_DEFINE_TYPE (EReflow, e_reflow, GNOME_TYPE_CANVAS_GROUP) + +enum { + PROP_0, + PROP_MINIMUM_WIDTH, + PROP_WIDTH, + PROP_HEIGHT, + PROP_EMPTY_MESSAGE, + PROP_MODEL, + PROP_COLUMN_WIDTH +}; + +enum { + SELECTION_EVENT, + COLUMN_WIDTH_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0, }; + +static GHashTable * +er_create_cmp_cache (gpointer user_data) +{ + EReflow *reflow = user_data; + return e_reflow_model_create_cmp_cache (reflow->model); +} + +static gint +er_compare (gint i1, + gint i2, + GHashTable *cmp_cache, + gpointer user_data) +{ + EReflow *reflow = user_data; + return e_reflow_model_compare (reflow->model, i1, i2, cmp_cache); +} + +static gint +e_reflow_pick_line (EReflow *reflow, + gdouble x) +{ + x += E_REFLOW_BORDER_WIDTH + E_REFLOW_DIVIDER_WIDTH; + x /= reflow->column_width + E_REFLOW_FULL_GUTTER; + return x; +} + +static gint +er_find_item (EReflow *reflow, + GnomeCanvasItem *item) +{ + gint i; + for (i = 0; i < reflow->count; i++) { + if (reflow->items[i] == item) + return i; + } + return -1; +} + +static void +e_reflow_resize_children (GnomeCanvasItem *item) +{ + EReflow *reflow; + gint i; + gint count; + + reflow = E_REFLOW (item); + + count = reflow->count; + for (i = 0; i < count; i++) { + if (reflow->items[i]) + gnome_canvas_item_set ( + reflow->items[i], + "width", (gdouble) reflow->column_width, + NULL); + } +} + +static inline void +e_reflow_update_selection_row (EReflow *reflow, + gint row) +{ + if (reflow->items[row]) { + g_object_set ( + reflow->items[row], + "selected", e_selection_model_is_row_selected (E_SELECTION_MODEL (reflow->selection), row), + NULL); + } else if (e_selection_model_is_row_selected (E_SELECTION_MODEL (reflow->selection), row)) { + reflow->items[row] = e_reflow_model_incarnate (reflow->model, row, GNOME_CANVAS_GROUP (reflow)); + g_object_set ( + reflow->items[row], + "selected", e_selection_model_is_row_selected (E_SELECTION_MODEL (reflow->selection), row), + "width", (gdouble) reflow->column_width, + NULL); + } +} + +static void +e_reflow_update_selection (EReflow *reflow) +{ + gint i; + gint count; + + count = reflow->count; + for (i = 0; i < count; i++) { + e_reflow_update_selection_row (reflow, i); + } +} + +static void +selection_changed (ESelectionModel *selection, + EReflow *reflow) +{ + e_reflow_update_selection (reflow); +} + +static void +selection_row_changed (ESelectionModel *selection, + gint row, + EReflow *reflow) +{ + e_reflow_update_selection_row (reflow, row); +} + +static gboolean +do_adjustment (gpointer user_data) +{ + gint row; + GtkLayout *layout; + GtkAdjustment *adjustment; + gdouble page_size; + gdouble value, min_value, max_value; + EReflow *reflow = user_data; + + row = reflow->cursor_row; + if (row == -1) + return FALSE; + + layout = GTK_LAYOUT (GNOME_CANVAS_ITEM (reflow)->canvas); + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (layout)); + + value = gtk_adjustment_get_value (adjustment); + page_size = gtk_adjustment_get_page_size (adjustment); + + if ((!reflow->items) || (!reflow->items[row])) + return TRUE; + min_value = reflow->items[row]->x2 - page_size; + max_value = reflow->items[row]->x1; + + if (value < min_value) + value = min_value; + + if (value > max_value) + value = max_value; + + if (value != gtk_adjustment_get_value (adjustment)) + gtk_adjustment_set_value (adjustment, value); + + reflow->do_adjustment_idle_id = 0; + + return FALSE; +} + +static void +cursor_changed (ESelectionModel *selection, + gint row, + gint col, + EReflow *reflow) +{ + gint count = reflow->count; + gint old_cursor = reflow->cursor_row; + + if (old_cursor < count && old_cursor >= 0) { + if (reflow->items[old_cursor]) { + g_object_set ( + reflow->items[old_cursor], + "has_cursor", FALSE, + NULL); + } + } + + reflow->cursor_row = row; + + if (row < count && row >= 0) { + if (reflow->items[row]) { + g_object_set ( + reflow->items[row], + "has_cursor", TRUE, + NULL); + } else { + reflow->items[row] = e_reflow_model_incarnate (reflow->model, row, GNOME_CANVAS_GROUP (reflow)); + g_object_set ( + reflow->items[row], + "has_cursor", TRUE, + "width", (gdouble) reflow->column_width, + NULL); + } + } + + if (reflow->do_adjustment_idle_id == 0) + reflow->do_adjustment_idle_id = g_idle_add (do_adjustment, reflow); + +} + +static void +incarnate (EReflow *reflow) +{ + gint column_width; + gint first_column; + gint last_column; + gint first_cell; + gint last_cell; + gint i; + GtkLayout *layout; + GtkAdjustment *adjustment; + gdouble value; + gdouble page_size; + + layout = GTK_LAYOUT (GNOME_CANVAS_ITEM (reflow)->canvas); + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (layout)); + + value = gtk_adjustment_get_value (adjustment); + page_size = gtk_adjustment_get_page_size (adjustment); + + column_width = reflow->column_width; + + first_column = value - 1 + E_REFLOW_BORDER_WIDTH; + first_column /= column_width + E_REFLOW_FULL_GUTTER; + + last_column = value + page_size + 1 - E_REFLOW_BORDER_WIDTH - E_REFLOW_DIVIDER_WIDTH; + last_column /= column_width + E_REFLOW_FULL_GUTTER; + last_column++; + + if (first_column >= 0 && first_column < reflow->column_count) + first_cell = reflow->columns[first_column]; + else + first_cell = 0; + + if (last_column >= 0 && last_column < reflow->column_count) + last_cell = reflow->columns[last_column]; + else + last_cell = reflow->count; + + for (i = first_cell; i < last_cell; i++) { + gint unsorted = e_sorter_sorted_to_model (E_SORTER (reflow->sorter), i); + if (reflow->items[unsorted] == NULL) { + if (reflow->model) { + reflow->items[unsorted] = e_reflow_model_incarnate (reflow->model, unsorted, GNOME_CANVAS_GROUP (reflow)); + g_object_set ( + reflow->items[unsorted], + "selected", e_selection_model_is_row_selected (E_SELECTION_MODEL (reflow->selection), unsorted), + "width", (gdouble) reflow->column_width, + NULL); + } + } + } + reflow->incarnate_idle_id = 0; +} + +static gboolean +invoke_incarnate (gpointer user_data) +{ + EReflow *reflow = user_data; + incarnate (reflow); + return FALSE; +} + +static void +queue_incarnate (EReflow *reflow) +{ + if (reflow->incarnate_idle_id == 0) + reflow->incarnate_idle_id = + g_idle_add_full (25, invoke_incarnate, reflow, NULL); +} + +static void +reflow_columns (EReflow *reflow) +{ + GSList *list; + gint count; + gint start; + gint i; + gint column_count, column_start; + gdouble running_height; + + if (reflow->reflow_from_column <= 1) { + start = 0; + column_count = 1; + column_start = 0; + } + else { + /* we start one column before the earliest new entry, + * so we can handle the case where the new entry is + * inserted at the start of the column */ + column_start = reflow->reflow_from_column - 1; + start = reflow->columns[column_start]; + column_count = column_start + 1; + } + + list = NULL; + + running_height = E_REFLOW_BORDER_WIDTH; + + count = reflow->count - start; + for (i = start; i < count; i++) { + gint unsorted = e_sorter_sorted_to_model (E_SORTER (reflow->sorter), i); + if (i != 0 && running_height + reflow->heights[unsorted] + E_REFLOW_BORDER_WIDTH > reflow->height) { + list = g_slist_prepend (list, GINT_TO_POINTER (i)); + column_count++; + running_height = E_REFLOW_BORDER_WIDTH * 2 + reflow->heights[unsorted]; + } else + running_height += reflow->heights[unsorted] + E_REFLOW_BORDER_WIDTH; + } + + reflow->column_count = column_count; + reflow->columns = g_renew (int, reflow->columns, column_count); + column_count--; + + for (; list && column_count > column_start; column_count--) { + GSList *to_free; + reflow->columns[column_count] = GPOINTER_TO_INT (list->data); + to_free = list; + list = list->next; + g_slist_free_1 (to_free); + } + reflow->columns[column_start] = start; + + queue_incarnate (reflow); + + reflow->need_reflow_columns = FALSE; + reflow->reflow_from_column = -1; +} + +static void +item_changed (EReflowModel *model, + gint i, + EReflow *reflow) +{ + if (i < 0 || i >= reflow->count) + return; + + reflow->heights[i] = e_reflow_model_height (reflow->model, i, GNOME_CANVAS_GROUP (reflow)); + if (reflow->items[i] != NULL) + e_reflow_model_reincarnate (model, i, reflow->items[i]); + e_sorter_array_clean (reflow->sorter); + reflow->reflow_from_column = -1; + reflow->need_reflow_columns = TRUE; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (reflow)); +} + +static void +item_removed (EReflowModel *model, + gint i, + EReflow *reflow) +{ + gint c; + gint sorted; + + if (i < 0 || i >= reflow->count) + return; + + sorted = e_sorter_model_to_sorted (E_SORTER (reflow->sorter), i); + for (c = reflow->column_count - 1; c >= 0; c--) { + gint start_of_column = reflow->columns[c]; + + if (start_of_column <= sorted) { + if (reflow->reflow_from_column == -1 + || reflow->reflow_from_column > c) { + reflow->reflow_from_column = c; + } + break; + } + } + + if (reflow->items[i]) + g_object_run_dispose (G_OBJECT (reflow->items[i])); + + memmove (reflow->heights + i, reflow->heights + i + 1, (reflow->count - i - 1) * sizeof (gint)); + memmove (reflow->items + i, reflow->items + i + 1, (reflow->count - i - 1) * sizeof (GnomeCanvasItem *)); + + reflow->count--; + + reflow->heights[reflow->count] = 0; + reflow->items[reflow->count] = NULL; + + reflow->need_reflow_columns = TRUE; + set_empty (reflow); + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (reflow)); + + e_sorter_array_set_count (reflow->sorter, reflow->count); + + e_selection_model_simple_delete_rows (E_SELECTION_MODEL_SIMPLE (reflow->selection), i, 1); +} + +static void +items_inserted (EReflowModel *model, + gint position, + gint count, + EReflow *reflow) +{ + gint i, oldcount; + + if (position < 0 || position > reflow->count) + return; + + oldcount = reflow->count; + + reflow->count += count; + + if (reflow->count > reflow->allocated_count) { + while (reflow->count > reflow->allocated_count) + reflow->allocated_count += 256; + reflow->heights = g_renew (int, reflow->heights, reflow->allocated_count); + reflow->items = g_renew (GnomeCanvasItem *, reflow->items, reflow->allocated_count); + } + memmove (reflow->heights + position + count, reflow->heights + position, (reflow->count - position - count) * sizeof (gint)); + memmove (reflow->items + position + count, reflow->items + position, (reflow->count - position - count) * sizeof (GnomeCanvasItem *)); + for (i = position; i < position + count; i++) { + reflow->items[i] = NULL; + reflow->heights[i] = e_reflow_model_height (reflow->model, i, GNOME_CANVAS_GROUP (reflow)); + } + + e_selection_model_simple_set_row_count (E_SELECTION_MODEL_SIMPLE (reflow->selection), reflow->count); + if (position == oldcount) + e_sorter_array_append (reflow->sorter, count); + else + e_sorter_array_set_count (reflow->sorter, reflow->count); + + for (i = position; i < position + count; i++) { + gint sorted = e_sorter_model_to_sorted (E_SORTER (reflow->sorter), i); + gint c; + + for (c = reflow->column_count - 1; c >= 0; c--) { + gint start_of_column = reflow->columns[c]; + + if (start_of_column <= sorted) { + if (reflow->reflow_from_column == -1 + || reflow->reflow_from_column > c) { + reflow->reflow_from_column = c; + } + break; + } + } + } + + reflow->need_reflow_columns = TRUE; + set_empty (reflow); + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (reflow)); +} + +static void +model_changed (EReflowModel *model, + EReflow *reflow) +{ + gint i; + gint count; + gint oldcount; + + count = reflow->count; + oldcount = count; + + for (i = 0; i < count; i++) { + if (reflow->items[i]) + g_object_run_dispose (G_OBJECT (reflow->items[i])); + } + g_free (reflow->items); + g_free (reflow->heights); + reflow->count = e_reflow_model_count (model); + reflow->allocated_count = reflow->count; + reflow->items = g_new (GnomeCanvasItem *, reflow->count); + reflow->heights = g_new (int, reflow->count); + + count = reflow->count; + for (i = 0; i < count; i++) { + reflow->items[i] = NULL; + reflow->heights[i] = e_reflow_model_height (reflow->model, i, GNOME_CANVAS_GROUP (reflow)); + } + + e_selection_model_simple_set_row_count (E_SELECTION_MODEL_SIMPLE (reflow->selection), count); + e_sorter_array_set_count (reflow->sorter, reflow->count); + + reflow->need_reflow_columns = TRUE; + if (oldcount > reflow->count) + reflow_columns (reflow); + set_empty (reflow); + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (reflow)); +} + +static void +comparison_changed (EReflowModel *model, + EReflow *reflow) +{ + e_sorter_array_clean (reflow->sorter); + reflow->reflow_from_column = -1; + reflow->need_reflow_columns = TRUE; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (reflow)); +} + +static void +set_empty (EReflow *reflow) +{ + if (reflow->count == 0) { + if (reflow->empty_text) { + if (reflow->empty_message) { + gnome_canvas_item_set ( + reflow->empty_text, + "width", reflow->minimum_width, + "text", reflow->empty_message, + NULL); + e_canvas_item_move_absolute ( + reflow->empty_text, + reflow->minimum_width / 2, + 0); + } else { + g_object_run_dispose (G_OBJECT (reflow->empty_text)); + reflow->empty_text = NULL; + } + } else { + if (reflow->empty_message) { + reflow->empty_text = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (reflow), + e_text_get_type (), + "width", reflow->minimum_width, + "clip", TRUE, + "use_ellipsis", TRUE, + "justification", GTK_JUSTIFY_CENTER, + "text", reflow->empty_message, + NULL); + e_canvas_item_move_absolute ( + reflow->empty_text, + reflow->minimum_width / 2, + 0); + } + } + } else { + if (reflow->empty_text) { + g_object_run_dispose (G_OBJECT (reflow->empty_text)); + reflow->empty_text = NULL; + } + } +} + +static void +disconnect_model (EReflow *reflow) +{ + if (reflow->model == NULL) + return; + + g_signal_handler_disconnect ( + reflow->model, + reflow->model_changed_id); + g_signal_handler_disconnect ( + reflow->model, + reflow->comparison_changed_id); + g_signal_handler_disconnect ( + reflow->model, + reflow->model_items_inserted_id); + g_signal_handler_disconnect ( + reflow->model, + reflow->model_item_removed_id); + g_signal_handler_disconnect ( + reflow->model, + reflow->model_item_changed_id); + g_object_unref (reflow->model); + + reflow->model_changed_id = 0; + reflow->comparison_changed_id = 0; + reflow->model_items_inserted_id = 0; + reflow->model_item_removed_id = 0; + reflow->model_item_changed_id = 0; + reflow->model = NULL; +} + +static void +disconnect_selection (EReflow *reflow) +{ + if (reflow->selection == NULL) + return; + + g_signal_handler_disconnect ( + reflow->selection, + reflow->selection_changed_id); + g_signal_handler_disconnect ( + reflow->selection, + reflow->selection_row_changed_id); + g_signal_handler_disconnect ( + reflow->selection, + reflow->cursor_changed_id); + g_object_unref (reflow->selection); + + reflow->selection_changed_id = 0; + reflow->selection_row_changed_id = 0; + reflow->cursor_changed_id = 0; + reflow->selection = NULL; +} + +static void +connect_model (EReflow *reflow, + EReflowModel *model) +{ + if (reflow->model != NULL) + disconnect_model (reflow); + + if (model == NULL) + return; + + reflow->model = g_object_ref (model); + + reflow->model_changed_id = g_signal_connect ( + reflow->model, "model_changed", + G_CALLBACK (model_changed), reflow); + + reflow->comparison_changed_id = g_signal_connect ( + reflow->model, "comparison_changed", + G_CALLBACK (comparison_changed), reflow); + + reflow->model_items_inserted_id = g_signal_connect ( + reflow->model, "model_items_inserted", + G_CALLBACK (items_inserted), reflow); + + reflow->model_item_removed_id = g_signal_connect ( + reflow->model, "model_item_removed", + G_CALLBACK (item_removed), reflow); + + reflow->model_item_changed_id = g_signal_connect ( + reflow->model, "model_item_changed", + G_CALLBACK (item_changed), reflow); + + model_changed (model, reflow); +} + +static void +adjustment_changed (GtkAdjustment *adjustment, + EReflow *reflow) +{ + queue_incarnate (reflow); +} + +static void +disconnect_adjustment (EReflow *reflow) +{ + if (reflow->adjustment == NULL) + return; + + g_signal_handler_disconnect ( + reflow->adjustment, + reflow->adjustment_changed_id); + g_signal_handler_disconnect ( + reflow->adjustment, + reflow->adjustment_value_changed_id); + + g_object_unref (reflow->adjustment); + + reflow->adjustment_changed_id = 0; + reflow->adjustment_value_changed_id = 0; + reflow->adjustment = NULL; +} + +static void +connect_adjustment (EReflow *reflow, + GtkAdjustment *adjustment) +{ + if (reflow->adjustment != NULL) + disconnect_adjustment (reflow); + + if (adjustment == NULL) + return; + + reflow->adjustment = g_object_ref (adjustment); + + reflow->adjustment_changed_id = g_signal_connect ( + adjustment, "changed", + G_CALLBACK (adjustment_changed), reflow); + + reflow->adjustment_value_changed_id = g_signal_connect ( + adjustment, "value_changed", + G_CALLBACK (adjustment_changed), reflow); +} + +#if 0 +static void +set_scroll_adjustments (GtkLayout *layout, + GtkAdjustment *hadj, + GtkAdjustment *vadj, + EReflow *reflow) +{ + connect_adjustment (reflow, hadj); +} + +static void +connect_set_adjustment (EReflow *reflow) +{ + reflow->set_scroll_adjustments_id = g_signal_connect ( + GNOME_CANVAS_ITEM (reflow)->canvas, "set_scroll_adjustments", + G_CALLBACK (set_scroll_adjustments), reflow); +} +#endif + +static void +disconnect_set_adjustment (EReflow *reflow) +{ + if (reflow->set_scroll_adjustments_id != 0) { + g_signal_handler_disconnect ( + GNOME_CANVAS_ITEM (reflow)->canvas, + reflow->set_scroll_adjustments_id); + reflow->set_scroll_adjustments_id = 0; + } +} + +static void +column_width_changed (EReflow *reflow) +{ + g_signal_emit (reflow, signals[COLUMN_WIDTH_CHANGED], 0, reflow->column_width); +} + +/* Virtual functions */ +static void +e_reflow_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GnomeCanvasItem *item; + EReflow *reflow; + + item = GNOME_CANVAS_ITEM (object); + reflow = E_REFLOW (object); + + switch (property_id) { + case PROP_HEIGHT: + reflow->height = g_value_get_double (value); + reflow->need_reflow_columns = TRUE; + e_canvas_item_request_reflow (item); + break; + case PROP_MINIMUM_WIDTH: + reflow->minimum_width = g_value_get_double (value); + if (item->flags & GNOME_CANVAS_ITEM_REALIZED) + set_empty (reflow); + e_canvas_item_request_reflow (item); + break; + case PROP_EMPTY_MESSAGE: + g_free (reflow->empty_message); + reflow->empty_message = g_strdup (g_value_get_string (value)); + if (item->flags & GNOME_CANVAS_ITEM_REALIZED) + set_empty (reflow); + break; + case PROP_MODEL: + connect_model (reflow, (EReflowModel *) g_value_get_object (value)); + break; + case PROP_COLUMN_WIDTH: + if (reflow->column_width != g_value_get_double (value)) { + GtkLayout *layout; + GtkAdjustment *adjustment; + gdouble old_width = reflow->column_width; + gdouble step_increment; + gdouble page_size; + + layout = GTK_LAYOUT (item->canvas); + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (layout)); + page_size = gtk_adjustment_get_page_size (adjustment); + + reflow->column_width = g_value_get_double (value); + step_increment = (reflow->column_width + + E_REFLOW_FULL_GUTTER) / 2; + gtk_adjustment_set_step_increment ( + adjustment, step_increment); + gtk_adjustment_set_page_increment ( + adjustment, page_size - step_increment); + e_reflow_resize_children (item); + e_canvas_item_request_reflow (item); + + reflow->need_column_resize = TRUE; + gnome_canvas_item_request_update (item); + + if (old_width != reflow->column_width) + column_width_changed (reflow); + } + break; + } +} + +static void +e_reflow_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EReflow *reflow; + + reflow = E_REFLOW (object); + + switch (property_id) { + case PROP_MINIMUM_WIDTH: + g_value_set_double (value, reflow->minimum_width); + break; + case PROP_WIDTH: + g_value_set_double (value, reflow->width); + break; + case PROP_HEIGHT: + g_value_set_double (value, reflow->height); + break; + case PROP_EMPTY_MESSAGE: + g_value_set_string (value, reflow->empty_message); + break; + case PROP_MODEL: + g_value_set_object (value, reflow->model); + break; + case PROP_COLUMN_WIDTH: + g_value_set_double (value, reflow->column_width); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +e_reflow_dispose (GObject *object) +{ + EReflow *reflow = E_REFLOW (object); + + g_free (reflow->items); + g_free (reflow->heights); + g_free (reflow->columns); + + reflow->items = NULL; + reflow->heights = NULL; + reflow->columns = NULL; + reflow->count = 0; + reflow->allocated_count = 0; + + if (reflow->incarnate_idle_id) + g_source_remove (reflow->incarnate_idle_id); + reflow->incarnate_idle_id = 0; + + if (reflow->do_adjustment_idle_id) + g_source_remove (reflow->do_adjustment_idle_id); + reflow->do_adjustment_idle_id = 0; + + disconnect_model (reflow); + disconnect_selection (reflow); + + g_free (reflow->empty_message); + reflow->empty_message = NULL; + + if (reflow->sorter) { + g_object_unref (reflow->sorter); + reflow->sorter = NULL; + } + + G_OBJECT_CLASS (e_reflow_parent_class)->dispose (object); +} + +static void +e_reflow_realize (GnomeCanvasItem *item) +{ + EReflow *reflow; + GtkAdjustment *adjustment; + gdouble page_increment; + gdouble step_increment; + gdouble page_size; + gint count; + gint i; + + reflow = E_REFLOW (item); + + if (GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->realize) + (* GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->realize) (item); + + reflow->arrow_cursor = gdk_cursor_new (GDK_SB_H_DOUBLE_ARROW); + reflow->default_cursor = gdk_cursor_new (GDK_LEFT_PTR); + + count = reflow->count; + for (i = 0; i < count; i++) { + if (reflow->items[i]) + gnome_canvas_item_set ( + reflow->items[i], + "width", reflow->column_width, + NULL); + } + + set_empty (reflow); + + reflow->need_reflow_columns = TRUE; + e_canvas_item_request_reflow (item); + + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (item->canvas)); + +#if 0 + connect_set_adjustment (reflow); +#endif + connect_adjustment (reflow, adjustment); + + page_size = gtk_adjustment_get_page_size (adjustment); + step_increment = (reflow->column_width + E_REFLOW_FULL_GUTTER) / 2; + page_increment = page_size - step_increment; + gtk_adjustment_set_step_increment (adjustment, step_increment); + gtk_adjustment_set_page_increment (adjustment, page_increment); +} + +static void +e_reflow_unrealize (GnomeCanvasItem *item) +{ + EReflow *reflow; + + reflow = E_REFLOW (item); + + g_object_unref (reflow->arrow_cursor); + g_object_unref (reflow->default_cursor); + reflow->arrow_cursor = NULL; + reflow->default_cursor = NULL; + + g_free (reflow->columns); + reflow->columns = NULL; + + disconnect_set_adjustment (reflow); + disconnect_adjustment (reflow); + + if (GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->unrealize) + (* GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->unrealize) (item); +} + +static gboolean +e_reflow_event (GnomeCanvasItem *item, + GdkEvent *event) +{ + EReflow *reflow; + gint return_val = FALSE; + + reflow = E_REFLOW (item); + + switch (event->type) + { + case GDK_KEY_PRESS: + return_val = e_selection_model_key_press (reflow->selection, (GdkEventKey *) event); + break; +#if 0 + if (event->key.keyval == GDK_Tab || + event->key.keyval == GDK_KEY_KP_Tab || + event->key.keyval == GDK_ISO_Left_Tab) { + gint i; + gint count; + count = reflow->count; + for (i = 0; i < count; i++) { + gint unsorted = e_sorter_sorted_to_model (E_SORTER (reflow->sorter), i); + GnomeCanvasItem *item = reflow->items[unsorted]; + EFocus has_focus; + if (item) { + g_object_get ( + item, + "has_focus", &has_focus, + NULL); + if (has_focus) { + if (event->key.state & GDK_SHIFT_MASK) { + if (i == 0) + return FALSE; + i--; + } else { + if (i == count - 1) + return FALSE; + i++; + } + + unsorted = e_sorter_sorted_to_model (E_SORTER (reflow->sorter), i); + if (reflow->items[unsorted] == NULL) { + reflow->items[unsorted] = e_reflow_model_incarnate (reflow->model, unsorted, GNOME_CANVAS_GROUP (reflow)); + } + + item = reflow->items[unsorted]; + gnome_canvas_item_set ( + item, + "has_focus", (event->key.state & GDK_SHIFT_MASK) ? E_FOCUS_END : E_FOCUS_START, + NULL); + return TRUE; + } + } + } + } +#endif + case GDK_BUTTON_PRESS: + switch (event->button.button) + { + case 1: + { + GdkEventButton *button = (GdkEventButton *) event; + gdouble n_x; + n_x = button->x; + n_x += E_REFLOW_BORDER_WIDTH + E_REFLOW_DIVIDER_WIDTH; + n_x = fmod (n_x,(reflow->column_width + E_REFLOW_FULL_GUTTER)); + + if (button->y >= E_REFLOW_BORDER_WIDTH && button->y <= reflow->height - E_REFLOW_BORDER_WIDTH && n_x < E_REFLOW_FULL_GUTTER) { + /* don't allow to drag the first line*/ + if (e_reflow_pick_line (reflow, button->x) == 0) + return TRUE; + reflow->which_column_dragged = e_reflow_pick_line (reflow, button->x); + reflow->start_x = reflow->which_column_dragged * (reflow->column_width + E_REFLOW_FULL_GUTTER) - E_REFLOW_DIVIDER_WIDTH / 2; + reflow->temp_column_width = reflow->column_width; + reflow->column_drag = TRUE; + + gnome_canvas_item_grab ( + item, + GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK, + reflow->arrow_cursor, + button->device, + button->time); + + reflow->previous_temp_column_width = -1; + reflow->need_column_resize = TRUE; + gnome_canvas_item_request_update (item); + return TRUE; + } + } + break; + case 4: + { + GtkLayout *layout; + GtkAdjustment *adjustment; + gdouble new_value; + + layout = GTK_LAYOUT (item->canvas); + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (layout)); + new_value = gtk_adjustment_get_value (adjustment); + new_value -= gtk_adjustment_get_step_increment (adjustment); + gtk_adjustment_set_value (adjustment, new_value); + } + break; + case 5: + { + GtkLayout *layout; + GtkAdjustment *adjustment; + gdouble new_value; + gdouble page_size; + gdouble upper; + + layout = GTK_LAYOUT (item->canvas); + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (layout)); + new_value = gtk_adjustment_get_value (adjustment); + new_value += gtk_adjustment_get_step_increment (adjustment); + upper = gtk_adjustment_get_upper (adjustment); + page_size = gtk_adjustment_get_page_size (adjustment); + if (new_value > upper - page_size) + new_value = upper - page_size; + gtk_adjustment_set_value (adjustment, new_value); + } + break; + } + break; + case GDK_BUTTON_RELEASE: + if (reflow->column_drag) { + gdouble old_width = reflow->column_width; + GdkEventButton *button = (GdkEventButton *) event; + GtkAdjustment *adjustment; + GtkLayout *layout; + gdouble value; + + layout = GTK_LAYOUT (item->canvas); + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (layout)); + value = gtk_adjustment_get_value (adjustment); + + reflow->temp_column_width = reflow->column_width + + (button->x - reflow->start_x) / (reflow->which_column_dragged - e_reflow_pick_line (reflow, value)); + if (reflow->temp_column_width < 50) + reflow->temp_column_width = 50; + reflow->column_drag = FALSE; + if (old_width != reflow->temp_column_width) { + gdouble page_increment; + gdouble step_increment; + gdouble page_size; + + page_size = gtk_adjustment_get_page_size (adjustment); + gtk_adjustment_set_value (adjustment, value + e_reflow_pick_line (reflow, value) * (reflow->temp_column_width - reflow->column_width)); + reflow->column_width = reflow->temp_column_width; + step_increment = (reflow->column_width + E_REFLOW_FULL_GUTTER) / 2; + page_increment = page_size - step_increment; + gtk_adjustment_set_step_increment (adjustment, step_increment); + gtk_adjustment_set_page_increment (adjustment, page_increment); + e_reflow_resize_children (item); + e_canvas_item_request_reflow (item); + gnome_canvas_request_redraw (item->canvas, 0, 0, reflow->width, reflow->height); + column_width_changed (reflow); + } + reflow->need_column_resize = TRUE; + gnome_canvas_item_request_update (item); + gnome_canvas_item_ungrab (item, button->time); + return TRUE; + } + break; + case GDK_MOTION_NOTIFY: + if (reflow->column_drag) { + gdouble old_width = reflow->temp_column_width; + GdkEventMotion *motion = (GdkEventMotion *) event; + GtkAdjustment *adjustment; + GtkLayout *layout; + gdouble value; + + layout = GTK_LAYOUT (item->canvas); + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (layout)); + value = gtk_adjustment_get_value (adjustment); + + reflow->temp_column_width = reflow->column_width + + (motion->x - reflow->start_x) / (reflow->which_column_dragged - e_reflow_pick_line (reflow, value)); + if (reflow->temp_column_width < 50) + reflow->temp_column_width = 50; + if (old_width != reflow->temp_column_width) { + reflow->need_column_resize = TRUE; + gnome_canvas_item_request_update (item); + } + return TRUE; + } else { + GdkEventMotion *motion = (GdkEventMotion *) event; + GdkWindow *window; + gdouble n_x; + + n_x = motion->x; + n_x += E_REFLOW_BORDER_WIDTH + E_REFLOW_DIVIDER_WIDTH; + n_x = fmod (n_x,(reflow->column_width + E_REFLOW_FULL_GUTTER)); + + window = gtk_widget_get_window (GTK_WIDGET (item->canvas)); + + if (motion->y >= E_REFLOW_BORDER_WIDTH && motion->y <= reflow->height - E_REFLOW_BORDER_WIDTH && n_x < E_REFLOW_FULL_GUTTER) { + if (reflow->default_cursor_shown) { + gdk_window_set_cursor (window, reflow->arrow_cursor); + reflow->default_cursor_shown = FALSE; + } + } else + if (!reflow->default_cursor_shown) { + gdk_window_set_cursor (window, reflow->default_cursor); + reflow->default_cursor_shown = TRUE; + } + + } + break; + case GDK_ENTER_NOTIFY: + if (!reflow->column_drag) { + GdkEventCrossing *crossing = (GdkEventCrossing *) event; + GdkWindow *window; + gdouble n_x; + + n_x = crossing->x; + n_x += E_REFLOW_BORDER_WIDTH + E_REFLOW_DIVIDER_WIDTH; + n_x = fmod (n_x,(reflow->column_width + E_REFLOW_FULL_GUTTER)); + + window = gtk_widget_get_window (GTK_WIDGET (item->canvas)); + + if (crossing->y >= E_REFLOW_BORDER_WIDTH && crossing->y <= reflow->height - E_REFLOW_BORDER_WIDTH && n_x < E_REFLOW_FULL_GUTTER) { + if (reflow->default_cursor_shown) { + gdk_window_set_cursor (window, reflow->arrow_cursor); + reflow->default_cursor_shown = FALSE; + } + } + } + break; + case GDK_LEAVE_NOTIFY: + if (!reflow->column_drag) { + GdkEventCrossing *crossing = (GdkEventCrossing *) event; + GdkWindow *window; + gdouble n_x; + + n_x = crossing->x; + n_x += E_REFLOW_BORDER_WIDTH + E_REFLOW_DIVIDER_WIDTH; + n_x = fmod (n_x,(reflow->column_width + E_REFLOW_FULL_GUTTER)); + + window = gtk_widget_get_window (GTK_WIDGET (item->canvas)); + + if (!(crossing->y >= E_REFLOW_BORDER_WIDTH && crossing->y <= reflow->height - E_REFLOW_BORDER_WIDTH && n_x < E_REFLOW_FULL_GUTTER)) { + if (!reflow->default_cursor_shown) { + gdk_window_set_cursor (window, reflow->default_cursor); + reflow->default_cursor_shown = TRUE; + } + } + } + break; + default: + break; + } + if (return_val) + return return_val; + else if (GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->event) + return (* GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->event) (item, event); + else + return FALSE; +} + +static void +e_reflow_draw (GnomeCanvasItem *item, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height) +{ + GtkStyleContext *style_context; + GtkWidget *widget; + gint x_rect, y_rect, width_rect, height_rect; + gdouble running_width; + EReflow *reflow = E_REFLOW (item); + GdkRGBA color; + gint i; + gdouble column_width; + + if (GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->draw) + GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->draw (item, cr, x, y, width, height); + column_width = reflow->column_width; + running_width = E_REFLOW_BORDER_WIDTH + column_width + E_REFLOW_BORDER_WIDTH; + y_rect = E_REFLOW_BORDER_WIDTH; + width_rect = E_REFLOW_DIVIDER_WIDTH; + height_rect = reflow->height - (E_REFLOW_BORDER_WIDTH * 2); + + /* Compute first column to draw. */ + i = x; + i /= column_width + E_REFLOW_FULL_GUTTER; + running_width += i * (column_width + E_REFLOW_FULL_GUTTER); + + widget = GTK_WIDGET (item->canvas); + style_context = gtk_widget_get_style_context (widget); + + cairo_save (cr); + + gtk_style_context_get_background_color ( + style_context, GTK_STATE_FLAG_ACTIVE, &color); + gdk_cairo_set_source_rgba (cr, &color); + + for (; i < reflow->column_count; i++) { + if (running_width > x + width) + break; + x_rect = running_width; + + gtk_render_background ( + style_context, cr, + (gdouble) x_rect - x, + (gdouble) y_rect - y, + (gdouble) width_rect, + (gdouble) height_rect); + + running_width += E_REFLOW_DIVIDER_WIDTH + E_REFLOW_BORDER_WIDTH + column_width + E_REFLOW_BORDER_WIDTH; + } + + cairo_restore (cr); + + if (reflow->column_drag) { + GtkAdjustment *adjustment; + GtkLayout *layout; + gdouble value; + gint start_line; + + layout = GTK_LAYOUT (item->canvas); + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (layout)); + value = gtk_adjustment_get_value (adjustment); + + start_line = e_reflow_pick_line (reflow, value); + i = x - start_line * (column_width + E_REFLOW_FULL_GUTTER); + running_width = start_line * (column_width + E_REFLOW_FULL_GUTTER); + column_width = reflow->temp_column_width; + running_width -= start_line * (column_width + E_REFLOW_FULL_GUTTER); + i += start_line * (column_width + E_REFLOW_FULL_GUTTER); + running_width += E_REFLOW_BORDER_WIDTH + column_width + E_REFLOW_BORDER_WIDTH; + y_rect = E_REFLOW_BORDER_WIDTH; + width_rect = E_REFLOW_DIVIDER_WIDTH; + height_rect = reflow->height - (E_REFLOW_BORDER_WIDTH * 2); + + /* Compute first column to draw. */ + i /= column_width + E_REFLOW_FULL_GUTTER; + running_width += i * (column_width + E_REFLOW_FULL_GUTTER); + + cairo_save (cr); + + gtk_style_context_get_color ( + style_context, GTK_STATE_FLAG_NORMAL, &color); + gdk_cairo_set_source_rgba (cr, &color); + + for (; i < reflow->column_count; i++) { + if (running_width > x + width) + break; + x_rect = running_width; + cairo_rectangle ( + cr, + x_rect - x, + y_rect - y, + width_rect - 1, + height_rect - 1); + cairo_fill (cr); + running_width += E_REFLOW_DIVIDER_WIDTH + E_REFLOW_BORDER_WIDTH + column_width + E_REFLOW_BORDER_WIDTH; + } + + cairo_restore (cr); + } +} + +static void +e_reflow_update (GnomeCanvasItem *item, + const cairo_matrix_t *i2c, + gint flags) +{ + EReflow *reflow; + gdouble x0, x1, y0, y1; + + reflow = E_REFLOW (item); + + if (GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->update) + GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->update (item, i2c, flags); + + x0 = item->x1; + y0 = item->y1; + x1 = item->x2; + y1 = item->y2; + if (x1 < x0 + reflow->width) + x1 = x0 + reflow->width; + if (y1 < y0 + reflow->height) + y1 = y0 + reflow->height; + item->x2 = x1; + item->y2 = y1; + + if (reflow->need_height_update) { + x0 = item->x1; + y0 = item->y1; + x1 = item->x2; + y1 = item->y2; + if (x0 > 0) + x0 = 0; + if (y0 > 0) + y0 = 0; + if (x1 < E_REFLOW (item)->width) + x1 = E_REFLOW (item)->width; + if (x1 < E_REFLOW (item)->height) + x1 = E_REFLOW (item)->height; + + gnome_canvas_request_redraw (item->canvas, x0, y0, x1, y1); + reflow->need_height_update = FALSE; + } else if (reflow->need_column_resize) { + GtkLayout *layout; + GtkAdjustment *adjustment; + gint x_rect, y_rect, width_rect, height_rect; + gint start_line; + gdouble running_width; + gint i; + gdouble column_width; + gdouble value; + + layout = GTK_LAYOUT (item->canvas); + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (layout)); + value = gtk_adjustment_get_value (adjustment); + start_line = e_reflow_pick_line (reflow, value); + + if (reflow->previous_temp_column_width != -1) { + running_width = start_line * (reflow->column_width + E_REFLOW_FULL_GUTTER); + column_width = reflow->previous_temp_column_width; + running_width -= start_line * (column_width + E_REFLOW_FULL_GUTTER); + running_width += E_REFLOW_BORDER_WIDTH + column_width + E_REFLOW_BORDER_WIDTH; + y_rect = E_REFLOW_BORDER_WIDTH; + width_rect = E_REFLOW_DIVIDER_WIDTH; + height_rect = reflow->height - (E_REFLOW_BORDER_WIDTH * 2); + + for (i = 0; i < reflow->column_count; i++) { + x_rect = running_width; + gnome_canvas_request_redraw (item->canvas, x_rect, y_rect, x_rect + width_rect, y_rect + height_rect); + running_width += E_REFLOW_DIVIDER_WIDTH + E_REFLOW_BORDER_WIDTH + column_width + E_REFLOW_BORDER_WIDTH; + } + } + + if (reflow->temp_column_width != -1) { + running_width = start_line * (reflow->column_width + E_REFLOW_FULL_GUTTER); + column_width = reflow->temp_column_width; + running_width -= start_line * (column_width + E_REFLOW_FULL_GUTTER); + running_width += E_REFLOW_BORDER_WIDTH + column_width + E_REFLOW_BORDER_WIDTH; + y_rect = E_REFLOW_BORDER_WIDTH; + width_rect = E_REFLOW_DIVIDER_WIDTH; + height_rect = reflow->height - (E_REFLOW_BORDER_WIDTH * 2); + + for (i = 0; i < reflow->column_count; i++) { + x_rect = running_width; + gnome_canvas_request_redraw (item->canvas, x_rect, y_rect, x_rect + width_rect, y_rect + height_rect); + running_width += E_REFLOW_DIVIDER_WIDTH + E_REFLOW_BORDER_WIDTH + column_width + E_REFLOW_BORDER_WIDTH; + } + } + + reflow->previous_temp_column_width = reflow->temp_column_width; + reflow->need_column_resize = FALSE; + } +} + +static GnomeCanvasItem * +e_reflow_point (GnomeCanvasItem *item, + gdouble x, + gdouble y, + gint cx, + gint cy) +{ + GnomeCanvasItem *child = NULL; + + if (GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->point) + child = GNOME_CANVAS_ITEM_CLASS (e_reflow_parent_class)->point (item, x, y, cx, cy); + + return child ? child : item; +#if 0 + if (y >= E_REFLOW_BORDER_WIDTH && y <= reflow->height - E_REFLOW_BORDER_WIDTH) { + gfloat n_x; + n_x = x; + n_x += E_REFLOW_BORDER_WIDTH + E_REFLOW_DIVIDER_WIDTH; + n_x = fmod (n_x, (reflow->column_width + E_REFLOW_FULL_GUTTER)); + if (n_x < E_REFLOW_FULL_GUTTER) { + *actual_item = item; + return 0; + } + } + return distance; +#endif +} + +static void +e_reflow_reflow (GnomeCanvasItem *item, + gint flags) +{ + EReflow *reflow = E_REFLOW (item); + gdouble old_width; + gdouble running_width; + gdouble running_height; + gint next_column; + gint i; + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) + return; + + if (reflow->need_reflow_columns) { + reflow_columns (reflow); + } + + old_width = reflow->width; + + running_width = E_REFLOW_BORDER_WIDTH; + running_height = E_REFLOW_BORDER_WIDTH; + + next_column = 1; + + for (i = 0; i < reflow->count; i++) { + gint unsorted = e_sorter_sorted_to_model (E_SORTER (reflow->sorter), i); + if (next_column < reflow->column_count && i == reflow->columns[next_column]) { + running_height = E_REFLOW_BORDER_WIDTH; + running_width += reflow->column_width + E_REFLOW_FULL_GUTTER; + next_column++; + } + + if (unsorted >= 0 && reflow->items[unsorted]) { + e_canvas_item_move_absolute ( + GNOME_CANVAS_ITEM (reflow->items[unsorted]), + (gdouble) running_width, + (gdouble) running_height); + running_height += reflow->heights[unsorted] + E_REFLOW_BORDER_WIDTH; + } + } + reflow->width = running_width + reflow->column_width + E_REFLOW_BORDER_WIDTH; + if (reflow->width < reflow->minimum_width) + reflow->width = reflow->minimum_width; + if (old_width != reflow->width) + e_canvas_item_request_parent_reflow (item); +} + +static gint +e_reflow_selection_event_real (EReflow *reflow, + GnomeCanvasItem *item, + GdkEvent *event) +{ + gint row; + gint return_val = TRUE; + switch (event->type) { + case GDK_BUTTON_PRESS: + switch (event->button.button) { + case 1: /* Fall through. */ + case 2: + row = er_find_item (reflow, item); + if (event->button.button == 1) { + reflow->maybe_did_something = + e_selection_model_maybe_do_something (reflow->selection, row, 0, event->button.state); + reflow->maybe_in_drag = TRUE; + } else { + e_selection_model_do_something (reflow->selection, row, 0, event->button.state); + } + break; + case 3: + row = er_find_item (reflow, item); + e_selection_model_right_click_down (reflow->selection, row, 0, 0); + break; + default: + return_val = FALSE; + break; + } + break; + case GDK_BUTTON_RELEASE: + if (event->button.button == 1) { + if (reflow->maybe_in_drag) { + reflow->maybe_in_drag = FALSE; + if (!reflow->maybe_did_something) { + row = er_find_item (reflow, item); + e_selection_model_do_something (reflow->selection, row, 0, event->button.state); + } + } + } + break; + case GDK_KEY_PRESS: + return_val = e_selection_model_key_press (reflow->selection, (GdkEventKey *) event); + break; + default: + return_val = FALSE; + break; + } + + return return_val; +} + +static void +e_reflow_class_init (EReflowClass *class) +{ + GObjectClass *object_class; + GnomeCanvasItemClass *item_class; + + object_class = (GObjectClass *) class; + item_class = (GnomeCanvasItemClass *) class; + + object_class->set_property = e_reflow_set_property; + object_class->get_property = e_reflow_get_property; + object_class->dispose = e_reflow_dispose; + + /* GnomeCanvasItem method overrides */ + item_class->event = e_reflow_event; + item_class->realize = e_reflow_realize; + item_class->unrealize = e_reflow_unrealize; + item_class->draw = e_reflow_draw; + item_class->update = e_reflow_update; + item_class->point = e_reflow_point; + + class->selection_event = e_reflow_selection_event_real; + class->column_width_changed = NULL; + + g_object_class_install_property ( + object_class, + PROP_MINIMUM_WIDTH, + g_param_spec_double ( + "minimum_width", + "Minimum width", + "Minimum Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WIDTH, + g_param_spec_double ( + "width", + "Width", + "Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_HEIGHT, + g_param_spec_double ( + "height", + "Height", + "Height", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_EMPTY_MESSAGE, + g_param_spec_string ( + "empty_message", + "Empty message", + "Empty message", + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MODEL, + g_param_spec_object ( + "model", + "Reflow model", + "Reflow model", + E_TYPE_REFLOW_MODEL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_COLUMN_WIDTH, + g_param_spec_double ( + "column_width", + "Column width", + "Column width", + 0.0, G_MAXDOUBLE, 150.0, + G_PARAM_READWRITE)); + + signals[SELECTION_EVENT] = g_signal_new ( + "selection_event", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EReflowClass, selection_event), + NULL, NULL, + e_marshal_INT__OBJECT_BOXED, + G_TYPE_INT, 2, + G_TYPE_OBJECT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + signals[COLUMN_WIDTH_CHANGED] = g_signal_new ( + "column_width_changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EReflowClass, column_width_changed), + NULL, NULL, + g_cclosure_marshal_VOID__DOUBLE, + G_TYPE_NONE, 1, + G_TYPE_DOUBLE); +} + +static void +e_reflow_init (EReflow *reflow) +{ + reflow->model = NULL; + reflow->items = NULL; + reflow->heights = NULL; + reflow->count = 0; + + reflow->columns = NULL; + reflow->column_count = 0; + + reflow->empty_text = NULL; + reflow->empty_message = NULL; + + reflow->minimum_width = 10; + reflow->width = 10; + reflow->height = 10; + + reflow->column_width = 150; + + reflow->column_drag = FALSE; + + reflow->need_height_update = FALSE; + reflow->need_column_resize = FALSE; + reflow->need_reflow_columns = FALSE; + + reflow->maybe_did_something = FALSE; + reflow->maybe_in_drag = FALSE; + + reflow->default_cursor_shown = TRUE; + reflow->arrow_cursor = NULL; + reflow->default_cursor = NULL; + + reflow->cursor_row = -1; + + reflow->incarnate_idle_id = 0; + reflow->do_adjustment_idle_id = 0; + reflow->set_scroll_adjustments_id = 0; + + reflow->selection = E_SELECTION_MODEL (e_selection_model_simple_new ()); + reflow->sorter = e_sorter_array_new (er_create_cmp_cache, er_compare, reflow); + + g_object_set ( + reflow->selection, + "sorter", reflow->sorter, + NULL); + + reflow->selection_changed_id = g_signal_connect ( + reflow->selection, "selection_changed", + G_CALLBACK (selection_changed), reflow); + + reflow->selection_row_changed_id = g_signal_connect ( + reflow->selection, "selection_row_changed", + G_CALLBACK (selection_row_changed), reflow); + + reflow->cursor_changed_id = g_signal_connect ( + reflow->selection, "cursor_changed", + G_CALLBACK (cursor_changed), reflow); + + e_canvas_item_set_reflow_callback (GNOME_CANVAS_ITEM (reflow), e_reflow_reflow); +} diff --git a/e-util/e-reflow.h b/e-util/e-reflow.h new file mode 100644 index 0000000000..a891e98f38 --- /dev/null +++ b/e-util/e-reflow.h @@ -0,0 +1,145 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_REFLOW_H +#define E_REFLOW_H + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-reflow-model.h> +#include <e-util/e-selection-model.h> +#include <e-util/e-sorter-array.h> + +/* Standard GObject macros */ +#define E_TYPE_REFLOW \ + (e_reflow_get_type ()) +#define E_REFLOW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_REFLOW, EReflow)) +#define E_REFLOW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_REFLOW, EReflowClass)) +#define E_IS_REFLOW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_REFLOW)) +#define E_IS_REFLOW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_REFLOW)) +#define E_REFLOW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_REFLOW, EReflowClass)) + +G_BEGIN_DECLS + +typedef struct _EReflow EReflow; +typedef struct _EReflowClass EReflowClass; +typedef struct _EReflowPrivate EReflowPrivate; + +struct _EReflow { + GnomeCanvasGroup parent; + + /* item specific fields */ + EReflowModel *model; + guint model_changed_id; + guint comparison_changed_id; + guint model_items_inserted_id; + guint model_item_removed_id; + guint model_item_changed_id; + + ESelectionModel *selection; + guint selection_changed_id; + guint selection_row_changed_id; + guint cursor_changed_id; + ESorterArray *sorter; + + GtkAdjustment *adjustment; + guint adjustment_changed_id; + guint adjustment_value_changed_id; + guint set_scroll_adjustments_id; + + gint *heights; + GnomeCanvasItem **items; + gint count; + gint allocated_count; + + gint *columns; + gint column_count; /* Number of columnns */ + + GnomeCanvasItem *empty_text; + gchar *empty_message; + + gdouble minimum_width; + gdouble width; + gdouble height; + + gdouble column_width; + + gint incarnate_idle_id; + gint do_adjustment_idle_id; + + /* These are all for when the column is being dragged. */ + gdouble start_x; + gint which_column_dragged; + gdouble temp_column_width; + gdouble previous_temp_column_width; + + gint cursor_row; + + gint reflow_from_column; + + guint column_drag : 1; + + guint need_height_update : 1; + guint need_column_resize : 1; + guint need_reflow_columns : 1; + + guint default_cursor_shown : 1; + + guint maybe_did_something : 1; + guint maybe_in_drag : 1; + GdkCursor *arrow_cursor; + GdkCursor *default_cursor; +}; + +struct _EReflowClass +{ + GnomeCanvasGroupClass parent_class; + + gint (*selection_event) (EReflow *reflow, GnomeCanvasItem *item, GdkEvent *event); + void (*column_width_changed) (EReflow *reflow, gdouble width); +}; + +/* + * To be added to a reflow, an item must have the argument "width" as + * a Read/Write argument and "height" as a Read Only argument. It + * should also do an ECanvas parent reflow request if its size + * changes. + */ +GType e_reflow_get_type (void); + +G_END_DECLS + +#endif /* E_REFLOW_H */ diff --git a/e-util/e-rule-context.c b/e-util/e-rule-context.c new file mode 100644 index 0000000000..dc7ce8160d --- /dev/null +++ b/e-util/e-rule-context.c @@ -0,0 +1,1026 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <errno.h> + +#include <glib/gstdio.h> + +#include <gtk/gtk.h> + +#include <glib/gi18n.h> + +#include <libedataserver/libedataserver.h> + +#include "e-alert-dialog.h" +#include "e-filter-code.h" +#include "e-filter-color.h" +#include "e-filter-datespec.h" +#include "e-filter-file.h" +#include "e-filter-input.h" +#include "e-filter-int.h" +#include "e-filter-option.h" +#include "e-filter-rule.h" +#include "e-rule-context.h" +#include "e-xml-utils.h" + +#define E_RULE_CONTEXT_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_RULE_CONTEXT, ERuleContextPrivate)) + +struct _ERuleContextPrivate { + gint frozen; +}; + +enum { + RULE_ADDED, + RULE_REMOVED, + CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +struct _revert_data { + GHashTable *rules; + gint rank; +}; + +G_DEFINE_TYPE ( + ERuleContext, + e_rule_context, + G_TYPE_OBJECT) + +static void +rule_context_set_error (ERuleContext *context, + gchar *error) +{ + g_free (context->error); + context->error = error; +} + +static void +new_rule_response (GtkWidget *dialog, + gint button, + ERuleContext *context) +{ + if (button == GTK_RESPONSE_OK) { + EFilterRule *rule = g_object_get_data ((GObject *) dialog, "rule"); + gchar *user = g_object_get_data ((GObject *) dialog, "path"); + EAlert *alert = NULL; + + if (!e_filter_rule_validate (rule, &alert)) { + e_alert_run_dialog (GTK_WINDOW (dialog), alert); + g_object_unref (alert); + return; + } + + if (e_rule_context_find_rule (context, rule->name, rule->source)) { + e_alert_run_dialog_for_args ((GtkWindow *) dialog, + "filter:bad-name-notunique", + rule->name, NULL); + + return; + } + + g_object_ref (rule); + e_rule_context_add_rule (context, rule); + if (user) + e_rule_context_save (context, user); + } + + gtk_widget_destroy (dialog); +} + +static void +revert_rule_remove (gpointer key, + EFilterRule *rule, + ERuleContext *context) +{ + e_rule_context_remove_rule (context, rule); + g_object_unref (rule); +} + +static void +revert_source_remove (gpointer key, + struct _revert_data *rest_data, + ERuleContext *context) +{ + g_hash_table_foreach ( + rest_data->rules, (GHFunc) revert_rule_remove, context); + g_hash_table_destroy (rest_data->rules); + g_free (rest_data); +} + +static guint +source_hashf (const gchar *a) +{ + return (a != NULL) ? g_str_hash (a) : 0; +} + +static gint +source_eqf (const gchar *a, + const gchar *b) +{ + return (g_strcmp0 (a, b) == 0); +} + +static void +free_part_set (struct _part_set_map *map) +{ + g_free (map->name); + g_free (map); +} + +static void +free_rule_set (struct _rule_set_map *map) +{ + g_free (map->name); + g_free (map); +} + +static void +rule_context_finalize (GObject *obj) +{ + ERuleContext *context =(ERuleContext *) obj; + + g_list_foreach (context->rule_set_list, (GFunc) free_rule_set, NULL); + g_list_free (context->rule_set_list); + g_hash_table_destroy (context->rule_set_map); + + g_list_foreach (context->part_set_list, (GFunc) free_part_set, NULL); + g_list_free (context->part_set_list); + g_hash_table_destroy (context->part_set_map); + + g_free (context->error); + + g_list_foreach (context->parts, (GFunc) g_object_unref, NULL); + g_list_free (context->parts); + + g_list_foreach (context->rules, (GFunc) g_object_unref, NULL); + g_list_free (context->rules); + + G_OBJECT_CLASS (e_rule_context_parent_class)->finalize (obj); +} + +static gint +rule_context_load (ERuleContext *context, + const gchar *system, + const gchar *user) +{ + xmlNodePtr set, rule, root; + xmlDocPtr systemdoc, userdoc; + struct _part_set_map *part_map; + struct _rule_set_map *rule_map; + + rule_context_set_error (context, NULL); + + systemdoc = e_xml_parse_file (system); + if (systemdoc == NULL) { + gchar * err_msg; + + err_msg = g_strdup_printf ( + "Unable to load system rules '%s': %s", + system, g_strerror (errno)); + g_warning ("%s: %s", G_STRFUNC, err_msg); + rule_context_set_error (context, err_msg); + /* no need to free err_msg here */ + return -1; + } + + root = xmlDocGetRootElement (systemdoc); + if (root == NULL || strcmp ((gchar *) root->name, "filterdescription")) { + gchar * err_msg; + + err_msg = g_strdup_printf ( + "Unable to load system rules '%s': " + "Invalid format", system); + g_warning ("%s: %s", G_STRFUNC, err_msg); + rule_context_set_error (context, err_msg); + /* no need to free err_msg here */ + xmlFreeDoc (systemdoc); + return -1; + } + /* doesn't matter if this doens't exist */ + userdoc = NULL; + if (g_file_test (user, G_FILE_TEST_IS_REGULAR)) + userdoc = e_xml_parse_file (user); + + /* now parse structure */ + /* get rule parts */ + set = root->children; + while (set) { + part_map = g_hash_table_lookup (context->part_set_map, set->name); + if (part_map) { + rule = set->children; + while (rule) { + if (!strcmp ((gchar *) rule->name, "part")) { + EFilterPart *part = + E_FILTER_PART (g_object_new ( + part_map->type, NULL, NULL)); + + if (e_filter_part_xml_create (part, rule, context) == 0) { + part_map->append (context, part); + } else { + g_object_unref (part); + g_warning ("Cannot load filter part"); + } + } + rule = rule->next; + } + } else if ((rule_map = g_hash_table_lookup ( + context->rule_set_map, set->name))) { + rule = set->children; + while (rule) { + if (!strcmp ((gchar *) rule->name, "rule")) { + EFilterRule *part = + E_FILTER_RULE (g_object_new ( + rule_map->type, NULL, NULL)); + + if (e_filter_rule_xml_decode (part, rule, context) == 0) { + part->system = TRUE; + rule_map->append (context, part); + } else { + g_object_unref (part); + g_warning ("Cannot load filter part"); + } + } + rule = rule->next; + } + } + set = set->next; + } + + /* now load actual rules */ + if (userdoc) { + root = xmlDocGetRootElement (userdoc); + set = root ? root->children : NULL; + while (set) { + rule_map = g_hash_table_lookup (context->rule_set_map, set->name); + if (rule_map) { + rule = set->children; + while (rule) { + if (!strcmp ((gchar *) rule->name, "rule")) { + EFilterRule *part = + E_FILTER_RULE (g_object_new ( + rule_map->type, NULL, NULL)); + + if (e_filter_rule_xml_decode (part, rule, context) == 0) { + rule_map->append (context, part); + } else { + g_object_unref (part); + g_warning ("Cannot load filter part"); + } + } + rule = rule->next; + } + } + set = set->next; + } + } + + xmlFreeDoc (userdoc); + xmlFreeDoc (systemdoc); + + return 0; +} + +static gint +rule_context_save (ERuleContext *context, + const gchar *user) +{ + xmlDocPtr doc; + xmlNodePtr root, rules, work; + GList *l; + EFilterRule *rule; + struct _rule_set_map *map; + gint ret; + + doc = xmlNewDoc ((xmlChar *)"1.0"); + /* FIXME: set character encoding to UTF-8? */ + root = xmlNewDocNode (doc, NULL, (xmlChar *)"filteroptions", NULL); + xmlDocSetRootElement (doc, root); + l = context->rule_set_list; + while (l) { + map = l->data; + rules = xmlNewDocNode (doc, NULL, (xmlChar *) map->name, NULL); + xmlAddChild (root, rules); + rule = NULL; + while ((rule = map->next (context, rule, NULL))) { + if (!rule->system) { + work = e_filter_rule_xml_encode (rule); + xmlAddChild (rules, work); + } + } + l = g_list_next (l); + } + + ret = e_xml_save_file (user, doc); + + xmlFreeDoc (doc); + + return ret; +} + +static gint +rule_context_revert (ERuleContext *context, + const gchar *user) +{ + xmlNodePtr set, rule; + /*struct _part_set_map *part_map;*/ + struct _rule_set_map *rule_map; + struct _revert_data *rest_data; + GHashTable *source_hash; + xmlDocPtr userdoc; + EFilterRule *frule; + + rule_context_set_error (context, NULL); + + userdoc = e_xml_parse_file (user); + if (userdoc == NULL) + /* clear out anythign we have? */ + return 0; + + source_hash = g_hash_table_new ( + (GHashFunc) source_hashf, + (GCompareFunc) source_eqf); + + /* setup stuff we have now */ + /* Note that we assume there is only 1 set of rules in a given rule context, + * although other parts of the code dont assume this */ + frule = NULL; + while ((frule = e_rule_context_next_rule (context, frule, NULL))) { + rest_data = g_hash_table_lookup (source_hash, frule->source); + if (rest_data == NULL) { + rest_data = g_malloc0 (sizeof (*rest_data)); + rest_data->rules = g_hash_table_new (g_str_hash, g_str_equal); + g_hash_table_insert (source_hash, frule->source, rest_data); + } + g_hash_table_insert (rest_data->rules, frule->name, frule); + } + + /* make what we have, match what we load */ + set = xmlDocGetRootElement (userdoc); + set = set ? set->children : NULL; + while (set) { + rule_map = g_hash_table_lookup (context->rule_set_map, set->name); + if (rule_map) { + rule = set->children; + while (rule) { + if (!strcmp ((gchar *) rule->name, "rule")) { + EFilterRule *part = + E_FILTER_RULE (g_object_new ( + rule_map->type, NULL, NULL)); + + if (e_filter_rule_xml_decode (part, rule, context) == 0) { + /* Use the revert data to keep + * track of the right rank of + * this rule part. */ + rest_data = g_hash_table_lookup (source_hash, part->source); + if (rest_data == NULL) { + rest_data = g_malloc0 (sizeof (*rest_data)); + rest_data->rules = g_hash_table_new ( + g_str_hash, + g_str_equal); + g_hash_table_insert ( + source_hash, + part->source, + rest_data); + } + frule = g_hash_table_lookup ( + rest_data->rules, + part->name); + if (frule) { + if (context->priv->frozen == 0 && + !e_filter_rule_eq (frule, part)) + e_filter_rule_copy (frule, part); + + g_object_unref (part); + e_rule_context_rank_rule ( + context, frule, + frule->source, + rest_data->rank); + g_hash_table_remove (rest_data->rules, frule->name); + } else { + e_rule_context_add_rule (context, part); + e_rule_context_rank_rule ( + context, + part, + part->source, + rest_data->rank); + } + rest_data->rank++; + } else { + g_object_unref (part); + g_warning ("Cannot load filter part"); + } + } + rule = rule->next; + } + } + set = set->next; + } + + xmlFreeDoc (userdoc); + + /* remove any we still have that weren't in the file */ + g_hash_table_foreach (source_hash, (GHFunc) revert_source_remove, context); + g_hash_table_destroy (source_hash); + + return 0; +} + +static EFilterElement * +rule_context_new_element (ERuleContext *context, + const gchar *type) +{ + if (!strcmp (type, "string")) { + return (EFilterElement *) e_filter_input_new (); + } else if (!strcmp (type, "address")) { + /* FIXME: temporary ... need real address type */ + return (EFilterElement *) e_filter_input_new_type_name (type); + } else if (!strcmp (type, "code")) { + return (EFilterElement *) e_filter_code_new (FALSE); + } else if (!strcmp (type, "rawcode")) { + return (EFilterElement *) e_filter_code_new (TRUE); + } else if (!strcmp (type, "colour")) { + return (EFilterElement *) e_filter_color_new (); + } else if (!strcmp (type, "optionlist")) { + return (EFilterElement *) e_filter_option_new (); + } else if (!strcmp (type, "datespec")) { + return (EFilterElement *) e_filter_datespec_new (); + } else if (!strcmp (type, "command")) { + return (EFilterElement *) e_filter_file_new_type_name (type); + } else if (!strcmp (type, "file")) { + return (EFilterElement *) e_filter_file_new_type_name (type); + } else if (!strcmp (type, "integer")) { + return (EFilterElement *) e_filter_int_new (); + } else if (!strcmp (type, "regex")) { + return (EFilterElement *) e_filter_input_new_type_name (type); + } else if (!strcmp (type, "completedpercent")) { + return (EFilterElement *) e_filter_int_new_type ( + "completedpercent", 0,100); + } else { + g_warning ("Unknown filter type '%s'", type); + return NULL; + } +} + +static void +e_rule_context_class_init (ERuleContextClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ERuleContextPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = rule_context_finalize; + + class->load = rule_context_load; + class->save = rule_context_save; + class->revert = rule_context_revert; + class->new_element = rule_context_new_element; + + signals[RULE_ADDED] = g_signal_new ( + "rule-added", + E_TYPE_RULE_CONTEXT, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ERuleContextClass, rule_added), + NULL, + NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + signals[RULE_REMOVED] = g_signal_new ( + "rule-removed", + E_TYPE_RULE_CONTEXT, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ERuleContextClass, rule_removed), + NULL, + NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + signals[CHANGED] = g_signal_new ( + "changed", + E_TYPE_RULE_CONTEXT, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ERuleContextClass, changed), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_rule_context_init (ERuleContext *context) +{ + context->priv = E_RULE_CONTEXT_GET_PRIVATE (context); + + context->part_set_map = g_hash_table_new (g_str_hash, g_str_equal); + context->rule_set_map = g_hash_table_new (g_str_hash, g_str_equal); + + context->flags = E_RULE_CONTEXT_GROUPING; +} + +/** + * e_rule_context_new: + * + * Create a new ERuleContext object. + * + * Return value: A new #ERuleContext object. + **/ +ERuleContext * +e_rule_context_new (void) +{ + return g_object_new (E_TYPE_RULE_CONTEXT, NULL); +} + +void +e_rule_context_add_part_set (ERuleContext *context, + const gchar *setname, + GType part_type, + ERuleContextPartFunc append, + ERuleContextNextPartFunc next) +{ + struct _part_set_map *map; + + g_return_if_fail (E_IS_RULE_CONTEXT (context)); + g_return_if_fail (setname != NULL); + g_return_if_fail (append != NULL); + g_return_if_fail (next != NULL); + + map = g_hash_table_lookup (context->part_set_map, setname); + if (map != NULL) { + g_hash_table_remove (context->part_set_map, setname); + context->part_set_list = g_list_remove (context->part_set_list, map); + free_part_set (map); + map = NULL; + } + + map = g_malloc0 (sizeof (*map)); + map->type = part_type; + map->append = append; + map->next = next; + map->name = g_strdup (setname); + g_hash_table_insert (context->part_set_map, map->name, map); + context->part_set_list = g_list_append (context->part_set_list, map); +} + +void +e_rule_context_add_rule_set (ERuleContext *context, + const gchar *setname, + GType rule_type, + ERuleContextRuleFunc append, + ERuleContextNextRuleFunc next) +{ + struct _rule_set_map *map; + + g_return_if_fail (E_IS_RULE_CONTEXT (context)); + g_return_if_fail (setname != NULL); + g_return_if_fail (append != NULL); + g_return_if_fail (next != NULL); + + map = g_hash_table_lookup (context->rule_set_map, setname); + if (map != NULL) { + g_hash_table_remove (context->rule_set_map, setname); + context->rule_set_list = g_list_remove (context->rule_set_list, map); + free_rule_set (map); + map = NULL; + } + + map = g_malloc0 (sizeof (*map)); + map->type = rule_type; + map->append = append; + map->next = next; + map->name = g_strdup (setname); + g_hash_table_insert (context->rule_set_map, map->name, map); + context->rule_set_list = g_list_append (context->rule_set_list, map); +} + +/** + * e_rule_context_load: + * @f: + * @system: + * @user: + * + * Load a rule context from a system and user description file. + * + * Return value: + **/ +gint +e_rule_context_load (ERuleContext *context, + const gchar *system, + const gchar *user) +{ + ERuleContextClass *class; + gint result; + + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), -1); + g_return_val_if_fail (system != NULL, -1); + g_return_val_if_fail (user != NULL, -1); + + class = E_RULE_CONTEXT_GET_CLASS (context); + g_return_val_if_fail (class->load != NULL, -1); + + context->priv->frozen++; + result = class->load (context, system, user); + context->priv->frozen--; + + return result; +} + +/** + * e_rule_context_save: + * @f: + * @user: + * + * Save a rule context to disk. + * + * Return value: + **/ +gint +e_rule_context_save (ERuleContext *context, + const gchar *user) +{ + ERuleContextClass *class; + + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), -1); + g_return_val_if_fail (user != NULL, -1); + + class = E_RULE_CONTEXT_GET_CLASS (context); + g_return_val_if_fail (class->save != NULL, -1); + + return class->save (context, user); +} + +/** + * e_rule_context_revert: + * @f: + * @user: + * + * Reverts a rule context from a user description file. Assumes the + * system description file is unchanged from when it was loaded. + * + * Return value: + **/ +gint +e_rule_context_revert (ERuleContext *context, + const gchar *user) +{ + ERuleContextClass *class; + + g_return_val_if_fail (E_RULE_CONTEXT (context), 0); + g_return_val_if_fail (user != NULL, 0); + + class = E_RULE_CONTEXT_GET_CLASS (context); + g_return_val_if_fail (class->revert != NULL, 0); + + return class->revert (context, user); +} + +EFilterPart * +e_rule_context_find_part (ERuleContext *context, + const gchar *name) +{ + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + g_return_val_if_fail (name != NULL, NULL); + + return e_filter_part_find_list (context->parts, name); +} + +EFilterPart * +e_rule_context_create_part (ERuleContext *context, + const gchar *name) +{ + EFilterPart *part; + + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + g_return_val_if_fail (name != NULL, NULL); + + part = e_rule_context_find_part (context, name); + + if (part == NULL) + return NULL; + + return e_filter_part_clone (part); +} + +EFilterPart * +e_rule_context_next_part (ERuleContext *context, + EFilterPart *last) +{ + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + + return e_filter_part_next_list (context->parts, last); +} + +EFilterRule * +e_rule_context_next_rule (ERuleContext *context, + EFilterRule *last, + const gchar *source) +{ + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + + return e_filter_rule_next_list (context->rules, last, source); +} + +EFilterRule * +e_rule_context_find_rule (ERuleContext *context, + const gchar *name, + const gchar *source) +{ + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + g_return_val_if_fail (name != NULL, NULL); + + return e_filter_rule_find_list (context->rules, name, source); +} + +void +e_rule_context_add_part (ERuleContext *context, + EFilterPart *part) +{ + g_return_if_fail (E_IS_RULE_CONTEXT (context)); + g_return_if_fail (E_IS_FILTER_PART (part)); + + context->parts = g_list_append (context->parts, part); +} + +void +e_rule_context_add_rule (ERuleContext *context, + EFilterRule *rule) +{ + g_return_if_fail (E_IS_RULE_CONTEXT (context)); + g_return_if_fail (E_IS_FILTER_RULE (rule)); + + context->rules = g_list_append (context->rules, rule); + + if (context->priv->frozen == 0) { + g_signal_emit (context, signals[RULE_ADDED], 0, rule); + g_signal_emit (context, signals[CHANGED], 0); + } +} + +/* Add a rule, with a gui, asking for confirmation first, + * and optionally save to path. */ +void +e_rule_context_add_rule_gui (ERuleContext *context, + EFilterRule *rule, + const gchar *title, + const gchar *path) +{ + GtkDialog *dialog; + GtkWidget *widget; + GtkWidget *content_area; + + g_return_if_fail (E_IS_RULE_CONTEXT (context)); + g_return_if_fail (E_IS_FILTER_RULE (rule)); + + widget = e_filter_rule_get_widget (rule, context); + gtk_widget_show (widget); + + dialog =(GtkDialog *) gtk_dialog_new (); + gtk_dialog_add_buttons ( + dialog, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, + NULL); + + gtk_window_set_title ((GtkWindow *) dialog, title); + gtk_window_set_default_size ((GtkWindow *) dialog, 600, 400); + gtk_window_set_resizable ((GtkWindow *) dialog, TRUE); + + content_area = gtk_dialog_get_content_area (dialog); + gtk_box_pack_start (GTK_BOX (content_area), widget, TRUE, TRUE, 0); + + g_object_set_data_full ((GObject *) dialog, "rule", rule, g_object_unref); + if (path) + g_object_set_data_full ((GObject *) dialog, "path", g_strdup (path), g_free); + + g_signal_connect ( + dialog, "response", + G_CALLBACK (new_rule_response), context); + + g_object_ref (context); + + g_object_set_data_full ((GObject *) dialog, "context", context, g_object_unref); + + gtk_widget_show ((GtkWidget *) dialog); +} + +void +e_rule_context_remove_rule (ERuleContext *context, + EFilterRule *rule) +{ + g_return_if_fail (E_IS_RULE_CONTEXT (context)); + g_return_if_fail (E_IS_FILTER_RULE (rule)); + + context->rules = g_list_remove (context->rules, rule); + + if (context->priv->frozen == 0) { + g_signal_emit (context, signals[RULE_REMOVED], 0, rule); + g_signal_emit (context, signals[CHANGED], 0); + } +} + +void +e_rule_context_rank_rule (ERuleContext *context, + EFilterRule *rule, + const gchar *source, + gint rank) +{ + GList *node; + gint i = 0, index = 0; + + g_return_if_fail (E_IS_RULE_CONTEXT (context)); + g_return_if_fail (E_IS_FILTER_RULE (rule)); + + if (e_rule_context_get_rank_rule (context, rule, source) == rank) + return; + + context->rules = g_list_remove (context->rules, rule); + node = context->rules; + while (node) { + EFilterRule *r = node->data; + + if (i == rank) { + context->rules = g_list_insert (context->rules, rule, index); + if (context->priv->frozen == 0) + g_signal_emit (context, signals[CHANGED], 0); + + return; + } + + index++; + if (source == NULL || (r->source && strcmp (r->source, source) == 0)) + i++; + + node = node->next; + } + + context->rules = g_list_append (context->rules, rule); + if (context->priv->frozen == 0) + g_signal_emit (context, signals[CHANGED], 0); +} + +gint +e_rule_context_get_rank_rule (ERuleContext *context, + EFilterRule *rule, + const gchar *source) +{ + GList *node; + gint i = 0; + + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), -1); + g_return_val_if_fail (E_IS_FILTER_RULE (rule), -1); + + node = context->rules; + while (node) { + EFilterRule *r = node->data; + + if (r == rule) + return i; + + if (source == NULL || (r->source && strcmp (r->source, source) == 0)) + i++; + + node = node->next; + } + + return -1; +} + +EFilterRule * +e_rule_context_find_rank_rule (ERuleContext *context, + gint rank, + const gchar *source) +{ + GList *node; + gint i = 0; + + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + + node = context->rules; + while (node) { + EFilterRule *r = node->data; + + if (source == NULL || (r->source && strcmp (r->source, source) == 0)) { + if (rank == i) + return r; + i++; + } + + node = node->next; + } + + return NULL; +} + +GList * +e_rule_context_rename_uri (ERuleContext *context, + const gchar *old_uri, + const gchar *new_uri, + GCompareFunc compare) +{ + ERuleContextClass *class; + + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + g_return_val_if_fail (old_uri != NULL, NULL); + g_return_val_if_fail (new_uri != NULL, NULL); + g_return_val_if_fail (compare != NULL, NULL); + + class = E_RULE_CONTEXT_GET_CLASS (context); + + /* This method is optional. */ + if (class->rename_uri == NULL) + return NULL; + + return class->rename_uri (context, old_uri, new_uri, compare); +} + +GList * +e_rule_context_delete_uri (ERuleContext *context, + const gchar *uri, + GCompareFunc compare) +{ + ERuleContextClass *class; + + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + g_return_val_if_fail (uri != NULL, NULL); + g_return_val_if_fail (compare != NULL, NULL); + + class = E_RULE_CONTEXT_GET_CLASS (context); + + /* This method is optional. */ + if (class->delete_uri == NULL) + return NULL; + + return class->delete_uri (context, uri, compare); +} + +void +e_rule_context_free_uri_list (ERuleContext *context, + GList *uris) +{ + g_return_if_fail (E_IS_RULE_CONTEXT (context)); + + /* TODO: should be virtual */ + + g_list_foreach (uris, (GFunc) g_free, NULL); + g_list_free (uris); +} + +/** + * e_rule_context_new_element: + * @context: + * @name: + * + * create a new filter element based on name. + * + * Return value: + **/ +EFilterElement * +e_rule_context_new_element (ERuleContext *context, + const gchar *name) +{ + ERuleContextClass *class; + + g_return_val_if_fail (E_IS_RULE_CONTEXT (context), NULL); + g_return_val_if_fail (name != NULL, NULL); + + class = E_RULE_CONTEXT_GET_CLASS (context); + g_return_val_if_fail (class->new_element != NULL, NULL); + + return class->new_element (context, name); +} diff --git a/e-util/e-rule-context.h b/e-util/e-rule-context.h new file mode 100644 index 0000000000..f543edd187 --- /dev/null +++ b/e-util/e-rule-context.h @@ -0,0 +1,218 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_RULE_CONTEXT_H +#define E_RULE_CONTEXT_H + +#include <libxml/parser.h> + +#include <e-util/e-filter-part.h> +#include <e-util/e-filter-rule.h> + +/* Standard GObject macros */ +#define E_TYPE_RULE_CONTEXT \ + (e_rule_context_get_type ()) +#define E_RULE_CONTEXT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_RULE_CONTEXT, ERuleContext)) +#define E_RULE_CONTEXT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_RULE_CONTEXT, ERuleContextClass)) +#define E_IS_RULE_CONTEXT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_RULE_CONTEXT)) +#define E_IS_RULE_CONTEXT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_RULE_CONTEXT)) +#define E_RULE_CONTEXT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_RULE_CONTEXT, ERuleContextClass)) + +G_BEGIN_DECLS + +typedef struct _ERuleContext ERuleContext; +typedef struct _ERuleContextClass ERuleContextClass; +typedef struct _ERuleContextPrivate ERuleContextPrivate; + +/* backend capabilities, this is a hack since we don't support nested rules */ +enum { + E_RULE_CONTEXT_GROUPING = 1 << 0, + E_RULE_CONTEXT_THREADING = 1 << 1 +}; + +typedef void (*ERuleContextRegisterFunc) (ERuleContext *context, + EFilterRule *rule, + gpointer user_data); +typedef void (*ERuleContextPartFunc) (ERuleContext *context, + EFilterPart *part); +typedef void (*ERuleContextRuleFunc) (ERuleContext *context, + EFilterRule *part); +typedef EFilterPart * + (*ERuleContextNextPartFunc) (ERuleContext *context, + EFilterPart *part); +typedef EFilterRule * + (*ERuleContextNextRuleFunc) (ERuleContext *context, + EFilterRule *rule, + const gchar *source); + +struct _ERuleContext { + GObject parent; + ERuleContextPrivate *priv; + + gchar *error; /* string version of error */ + + guint32 flags; /* capability flags */ + + GList *parts; + GList *rules; + + GHashTable *part_set_map; /* map set types to part types */ + GList *part_set_list; + GHashTable *rule_set_map; /* map set types to rule types */ + GList *rule_set_list; +}; + +struct _ERuleContextClass { + GObjectClass parent_class; + + /* methods */ + gint (*load) (ERuleContext *context, + const gchar *system, + const gchar *user); + gint (*save) (ERuleContext *context, + const gchar *user); + gint (*revert) (ERuleContext *context, + const gchar *user); + + GList * (*delete_uri) (ERuleContext *context, + const gchar *uri, + GCompareFunc compare_func); + GList * (*rename_uri) (ERuleContext *context, + const gchar *old_uri, + const gchar *new_uri, + GCompareFunc compare_func); + + EFilterElement *(*new_element) (ERuleContext *context, + const gchar *name); + + /* signals */ + void (*rule_added) (ERuleContext *context, + EFilterRule *rule); + void (*rule_removed) (ERuleContext *context, + EFilterRule *rule); + void (*changed) (ERuleContext *context); +}; + +struct _part_set_map { + gchar *name; + GType type; + ERuleContextPartFunc append; + ERuleContextNextPartFunc next; +}; + +struct _rule_set_map { + gchar *name; + GType type; + ERuleContextRuleFunc append; + ERuleContextNextRuleFunc next; +}; + +GType e_rule_context_get_type (void); +ERuleContext * e_rule_context_new (void); + +gint e_rule_context_load (ERuleContext *context, + const gchar *system, + const gchar *user); +gint e_rule_context_save (ERuleContext *context, + const gchar *user); +gint e_rule_context_revert (ERuleContext *context, + const gchar *user); + +void e_rule_context_add_part (ERuleContext *context, + EFilterPart *part); +EFilterPart * e_rule_context_find_part (ERuleContext *context, + const gchar *name); +EFilterPart * e_rule_context_create_part (ERuleContext *context, + const gchar *name); +EFilterPart * e_rule_context_next_part (ERuleContext *context, + EFilterPart *last); + +EFilterRule * e_rule_context_next_rule (ERuleContext *context, + EFilterRule *last, + const gchar *source); +EFilterRule * e_rule_context_find_rule (ERuleContext *context, + const gchar *name, + const gchar *source); +EFilterRule * e_rule_context_find_rank_rule (ERuleContext *context, + gint rank, + const gchar *source); +void e_rule_context_add_rule (ERuleContext *context, + EFilterRule *rule); +void e_rule_context_add_rule_gui (ERuleContext *context, + EFilterRule *rule, + const gchar *title, + const gchar *path); +void e_rule_context_remove_rule (ERuleContext *context, + EFilterRule *rule); + +void e_rule_context_rank_rule (ERuleContext *context, + EFilterRule *rule, + const gchar *source, + gint rank); +gint e_rule_context_get_rank_rule (ERuleContext *context, + EFilterRule *rule, + const gchar *source); + +void e_rule_context_add_part_set (ERuleContext *context, + const gchar *setname, + GType part_type, + ERuleContextPartFunc append, + ERuleContextNextPartFunc next); +void e_rule_context_add_rule_set (ERuleContext *context, + const gchar *setname, + GType rule_type, + ERuleContextRuleFunc append, + ERuleContextNextRuleFunc next); + +EFilterElement *e_rule_context_new_element (ERuleContext *context, + const gchar *name); + +GList * e_rule_context_delete_uri (ERuleContext *context, + const gchar *uri, + GCompareFunc compare); +GList * e_rule_context_rename_uri (ERuleContext *context, + const gchar *old_uri, + const gchar *new_uri, + GCompareFunc compare); + +void e_rule_context_free_uri_list (ERuleContext *context, + GList *uris); + +G_END_DECLS + +#endif /* E_RULE_CONTEXT_H */ diff --git a/e-util/e-rule-editor.c b/e-util/e-rule-editor.c new file mode 100644 index 0000000000..c063ae41ae --- /dev/null +++ b/e-util/e-rule-editor.c @@ -0,0 +1,920 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-rule-editor.h" + +/* for getenv only, remove when getenv need removed */ +#include <stdlib.h> +#include <string.h> + +#include <glib/gi18n.h> + +#include "e-alert-dialog.h" +#include "e-misc-utils.h" + +#define E_RULE_EDITOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_RULE_EDITOR, ERuleEditorPrivate)) + +static gint enable_undo = 0; + +enum { + BUTTON_ADD, + BUTTON_EDIT, + BUTTON_DELETE, + BUTTON_TOP, + BUTTON_UP, + BUTTON_DOWN, + BUTTON_BOTTOM, + BUTTON_LAST +}; + +struct _ERuleEditorPrivate { + GtkButton *buttons[BUTTON_LAST]; +}; + +G_DEFINE_TYPE ( + ERuleEditor, + e_rule_editor, + GTK_TYPE_DIALOG) + +static void +rule_editor_add_undo (ERuleEditor *editor, + gint type, + EFilterRule *rule, + gint rank, + gint newrank) +{ + ERuleEditorUndo *undo; + + if (!editor->undo_active && enable_undo) { + undo = g_malloc0 (sizeof (*undo)); + undo->rule = rule; + undo->type = type; + undo->rank = rank; + undo->newrank = newrank; + + undo->next = editor->undo_log; + editor->undo_log = undo; + } else { + g_object_unref (rule); + } +} + +static void +rule_editor_play_undo (ERuleEditor *editor) +{ + ERuleEditorUndo *undo, *next; + EFilterRule *rule; + + editor->undo_active = TRUE; + undo = editor->undo_log; + editor->undo_log = NULL; + while (undo) { + next = undo->next; + switch (undo->type) { + case E_RULE_EDITOR_LOG_EDIT: + rule = e_rule_context_find_rank_rule (editor->context, undo->rank, undo->rule->source); + if (rule) { + e_filter_rule_copy (rule, undo->rule); + } else { + g_warning ("Could not find the right rule to undo against?"); + } + break; + case E_RULE_EDITOR_LOG_ADD: + rule = e_rule_context_find_rank_rule (editor->context, undo->rank, undo->rule->source); + if (rule) + e_rule_context_remove_rule (editor->context, rule); + break; + case E_RULE_EDITOR_LOG_REMOVE: + g_object_ref (undo->rule); + e_rule_context_add_rule (editor->context, undo->rule); + e_rule_context_rank_rule (editor->context, undo->rule, editor->source, undo->rank); + break; + case E_RULE_EDITOR_LOG_RANK: + rule = e_rule_context_find_rank_rule (editor->context, undo->newrank, undo->rule->source); + if (rule) + e_rule_context_rank_rule (editor->context, rule, editor->source, undo->rank); + break; + } + + g_object_unref (undo->rule); + g_free (undo); + undo = next; + } + editor->undo_active = FALSE; +} + +static void +dialog_rule_changed (EFilterRule *fr, + GtkWidget *dialog) +{ + g_return_if_fail (dialog != NULL); + + gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog), GTK_RESPONSE_OK, fr && fr->parts); +} + +static void +add_editor_response (GtkWidget *dialog, + gint button, + ERuleEditor *editor) +{ + GtkTreeSelection *selection; + GtkTreePath *path; + GtkTreeIter iter; + + if (button == GTK_RESPONSE_OK) { + EAlert *alert = NULL; + if (!e_filter_rule_validate (editor->edit, &alert)) { + e_alert_run_dialog (GTK_WINDOW (dialog), alert); + g_object_unref (alert); + return; + } + + if (e_rule_context_find_rule (editor->context, editor->edit->name, editor->edit->source)) { + e_alert_run_dialog_for_args ( + GTK_WINDOW (dialog), + "filter:bad-name-notunique", + editor->edit->name, NULL); + return; + } + + g_object_ref (editor->edit); + + gtk_list_store_append (editor->model, &iter); + gtk_list_store_set ( + editor->model, &iter, + 0, editor->edit->name, + 1, editor->edit, + 2, editor->edit->enabled, -1); + selection = gtk_tree_view_get_selection (editor->list); + gtk_tree_selection_select_iter (selection, &iter); + + /* scroll to the newly added row */ + path = gtk_tree_model_get_path ( + GTK_TREE_MODEL (editor->model), &iter); + gtk_tree_view_scroll_to_cell ( + editor->list, path, NULL, TRUE, 1.0, 0.0); + gtk_tree_path_free (path); + + editor->current = editor->edit; + e_rule_context_add_rule (editor->context, editor->current); + + g_object_ref (editor->current); + rule_editor_add_undo ( + editor, + E_RULE_EDITOR_LOG_ADD, + editor->current, + e_rule_context_get_rank_rule ( + editor->context, + editor->current, + editor->current->source), + 0); + } + + gtk_widget_destroy (dialog); +} + +static void +editor_destroy (ERuleEditor *editor, + GObject *deadbeef) +{ + if (editor->edit) { + g_object_unref (editor->edit); + editor->edit = NULL; + } + + editor->dialog = NULL; + + gtk_widget_set_sensitive (GTK_WIDGET (editor), TRUE); + e_rule_editor_set_sensitive (editor); +} + +static gboolean +update_selected_rule (ERuleEditor *editor) +{ + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreeIter iter; + + selection = gtk_tree_view_get_selection (editor->list); + if (selection && gtk_tree_selection_get_selected (selection, &model, &iter)) { + gtk_tree_model_get (GTK_TREE_MODEL (editor->model), &iter, 1, &editor->current, -1); + return TRUE; + } + + return FALSE; +} + +static void +cursor_changed (GtkTreeView *treeview, + ERuleEditor *editor) +{ + if (update_selected_rule (editor)) { + g_return_if_fail (editor->current); + + e_rule_editor_set_sensitive (editor); + } +} + +static void +editor_response (GtkWidget *dialog, + gint button, + ERuleEditor *editor) +{ + if (button == GTK_RESPONSE_CANCEL) { + if (enable_undo) + rule_editor_play_undo (editor); + else { + ERuleEditorUndo *undo, *next; + + undo = editor->undo_log; + editor->undo_log = NULL; + while (undo) { + next = undo->next; + g_object_unref (undo->rule); + g_free (undo); + undo = next; + } + } + } +} + +static void +rule_add (GtkWidget *widget, + ERuleEditor *editor) +{ + GtkWidget *rules; + GtkWidget *content_area; + + if (editor->edit != NULL) + return; + + editor->edit = e_rule_editor_create_rule (editor); + e_filter_rule_set_source (editor->edit, editor->source); + rules = e_filter_rule_get_widget (editor->edit, editor->context); + + editor->dialog = gtk_dialog_new (); + gtk_dialog_add_buttons ( + GTK_DIALOG (editor->dialog), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, + NULL); + + gtk_window_set_title ((GtkWindow *) editor->dialog, _("Add Rule")); + gtk_window_set_default_size (GTK_WINDOW (editor->dialog), 650, 400); + gtk_window_set_resizable (GTK_WINDOW (editor->dialog), TRUE); + gtk_window_set_transient_for ((GtkWindow *) editor->dialog, (GtkWindow *) editor); + gtk_container_set_border_width ((GtkContainer *) editor->dialog, 6); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (editor->dialog)); + gtk_box_pack_start (GTK_BOX (content_area), rules, TRUE, TRUE, 3); + + g_signal_connect ( + editor->dialog, "response", + G_CALLBACK (add_editor_response), editor); + g_object_weak_ref ((GObject *) editor->dialog, (GWeakNotify) editor_destroy, editor); + + g_signal_connect ( + editor->edit, "changed", + G_CALLBACK (dialog_rule_changed), editor->dialog); + dialog_rule_changed (editor->edit, editor->dialog); + + gtk_widget_set_sensitive (GTK_WIDGET (editor), FALSE); + + gtk_widget_show (editor->dialog); +} + +static void +edit_editor_response (GtkWidget *dialog, + gint button, + ERuleEditor *editor) +{ + EFilterRule *rule; + GtkTreePath *path; + GtkTreeIter iter; + gint pos; + + if (button == GTK_RESPONSE_OK) { + EAlert *alert = NULL; + if (!e_filter_rule_validate (editor->edit, &alert)) { + e_alert_run_dialog (GTK_WINDOW (dialog), alert); + g_object_unref (alert); + return; + } + + rule = e_rule_context_find_rule ( + editor->context, + editor->edit->name, + editor->edit->source); + + if (rule != NULL && rule != editor->current) { + e_alert_run_dialog_for_args ( + GTK_WINDOW (dialog), + "filter:bad-name-notunique", + rule->name, NULL); + return; + } + + pos = e_rule_context_get_rank_rule ( + editor->context, + editor->current, + editor->source); + + if (pos != -1) { + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, pos); + gtk_tree_model_get_iter ( + GTK_TREE_MODEL (editor->model), &iter, path); + gtk_tree_path_free (path); + + gtk_list_store_set ( + editor->model, &iter, + 0, editor->edit->name, -1); + + rule_editor_add_undo ( + editor, E_RULE_EDITOR_LOG_EDIT, + e_filter_rule_clone (editor->current), + pos, 0); + + /* replace the old rule with the new rule */ + e_filter_rule_copy (editor->current, editor->edit); + } + } + + gtk_widget_destroy (dialog); +} + +static void +rule_edit (GtkWidget *widget, + ERuleEditor *editor) +{ + GtkWidget *rules; + GtkWidget *content_area; + + update_selected_rule (editor); + + if (editor->current == NULL || editor->edit != NULL) + return; + + editor->edit = e_filter_rule_clone (editor->current); + + rules = e_filter_rule_get_widget (editor->edit, editor->context); + + editor->dialog = gtk_dialog_new (); + gtk_dialog_add_buttons ( + (GtkDialog *) editor->dialog, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, + NULL); + + gtk_window_set_title ((GtkWindow *) editor->dialog, _("Edit Rule")); + gtk_window_set_default_size (GTK_WINDOW (editor->dialog), 650, 400); + gtk_window_set_resizable (GTK_WINDOW (editor->dialog), TRUE); + gtk_window_set_transient_for (GTK_WINDOW (editor->dialog), GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (editor)))); + gtk_container_set_border_width ((GtkContainer *) editor->dialog, 6); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (editor->dialog)); + gtk_box_pack_start (GTK_BOX (content_area), rules, TRUE, TRUE, 3); + + g_signal_connect ( + editor->dialog, "response", + G_CALLBACK (edit_editor_response), editor); + g_object_weak_ref ((GObject *) editor->dialog, (GWeakNotify) editor_destroy, editor); + + g_signal_connect ( + editor->edit, "changed", + G_CALLBACK (dialog_rule_changed), editor->dialog); + dialog_rule_changed (editor->edit, editor->dialog); + + gtk_widget_set_sensitive (GTK_WIDGET (editor), FALSE); + + gtk_widget_show (editor->dialog); +} + +static void +rule_delete (GtkWidget *widget, + ERuleEditor *editor) +{ + GtkTreeSelection *selection; + GtkTreePath *path; + GtkTreeIter iter; + gint pos, len; + + update_selected_rule (editor); + + pos = e_rule_context_get_rank_rule (editor->context, editor->current, editor->source); + if (pos != -1) { + EFilterRule *delete_rule = editor->current; + + editor->current = NULL; + + e_rule_context_remove_rule (editor->context, delete_rule); + + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, pos); + gtk_tree_model_get_iter (GTK_TREE_MODEL (editor->model), &iter, path); + gtk_list_store_remove (editor->model, &iter); + gtk_tree_path_free (path); + + rule_editor_add_undo ( + editor, + E_RULE_EDITOR_LOG_REMOVE, + delete_rule, + e_rule_context_get_rank_rule ( + editor->context, + delete_rule, + delete_rule->source), + 0); +#if 0 + g_object_unref (delete_rule); +#endif + + /* now select the next rule */ + len = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (editor->model), NULL); + pos = pos >= len ? len - 1 : pos; + + if (pos >= 0) { + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, pos); + gtk_tree_model_get_iter (GTK_TREE_MODEL (editor->model), &iter, path); + gtk_tree_path_free (path); + + /* select the new row */ + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (editor->list)); + gtk_tree_selection_select_iter (selection, &iter); + + /* scroll to the selected row */ + path = gtk_tree_model_get_path ((GtkTreeModel *) editor->model, &iter); + gtk_tree_view_scroll_to_cell (editor->list, path, NULL, FALSE, 0.0, 0.0); + gtk_tree_path_free (path); + + /* update our selection state */ + cursor_changed (editor->list, editor); + return; + } + } + + e_rule_editor_set_sensitive (editor); +} + +static void +rule_move (ERuleEditor *editor, + gint from, + gint to) +{ + GtkTreeSelection *selection; + GtkTreePath *path; + GtkTreeIter iter; + EFilterRule *rule; + + rule_editor_add_undo ( + editor, E_RULE_EDITOR_LOG_RANK, + g_object_ref (editor->current), + e_rule_context_get_rank_rule (editor->context, + editor->current, editor->source), to); + + e_rule_context_rank_rule ( + editor->context, editor->current, editor->source, to); + + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, from); + gtk_tree_model_get_iter (GTK_TREE_MODEL (editor->model), &iter, path); + gtk_tree_path_free (path); + + gtk_tree_model_get (GTK_TREE_MODEL (editor->model), &iter, 1, &rule, -1); + g_return_if_fail (rule != NULL); + + /* remove and then re-insert the row at the new location */ + gtk_list_store_remove (editor->model, &iter); + gtk_list_store_insert (editor->model, &iter, to); + + /* set the data on the row */ + gtk_list_store_set (editor->model, &iter, 0, rule->name, 1, rule, 2, rule->enabled, -1); + + /* select the row */ + selection = gtk_tree_view_get_selection (editor->list); + gtk_tree_selection_select_iter (selection, &iter); + + /* scroll to the selected row */ + path = gtk_tree_model_get_path ((GtkTreeModel *) editor->model, &iter); + gtk_tree_view_scroll_to_cell (editor->list, path, NULL, FALSE, 0.0, 0.0); + gtk_tree_path_free (path); + + e_rule_editor_set_sensitive (editor); +} + +static void +rule_top (GtkWidget *widget, + ERuleEditor *editor) +{ + gint pos; + + update_selected_rule (editor); + + pos = e_rule_context_get_rank_rule ( + editor->context, editor->current, editor->source); + if (pos > 0) + rule_move (editor, pos, 0); +} + +static void +rule_up (GtkWidget *widget, + ERuleEditor *editor) +{ + gint pos; + + update_selected_rule (editor); + + pos = e_rule_context_get_rank_rule ( + editor->context, editor->current, editor->source); + if (pos > 0) + rule_move (editor, pos, pos - 1); +} + +static void +rule_down (GtkWidget *widget, + ERuleEditor *editor) +{ + gint pos; + + update_selected_rule (editor); + + pos = e_rule_context_get_rank_rule ( + editor->context, editor->current, editor->source); + if (pos >= 0) + rule_move (editor, pos, pos + 1); +} + +static void +rule_bottom (GtkWidget *widget, + ERuleEditor *editor) +{ + gint pos; + gint count = 0; + EFilterRule *rule = NULL; + + update_selected_rule (editor); + + pos = e_rule_context_get_rank_rule ( + editor->context, editor->current, editor->source); + /* There's probably a better/faster way to get the count of the list here */ + while ((rule = e_rule_context_next_rule (editor->context, rule, editor->source))) + count++; + count--; + if (pos >= 0) + rule_move (editor, pos, count); +} + +static struct { + const gchar *name; + GCallback func; +} edit_buttons[] = { + { "rule_add", G_CALLBACK (rule_add) }, + { "rule_edit", G_CALLBACK (rule_edit) }, + { "rule_delete", G_CALLBACK (rule_delete) }, + { "rule_top", G_CALLBACK (rule_top) }, + { "rule_up", G_CALLBACK (rule_up) }, + { "rule_down", G_CALLBACK (rule_down) }, + { "rule_bottom", G_CALLBACK (rule_bottom) }, +}; + +static void +rule_editor_finalize (GObject *object) +{ + ERuleEditor *editor = E_RULE_EDITOR (object); + ERuleEditorUndo *undo, *next; + + g_object_unref (editor->context); + + undo = editor->undo_log; + while (undo) { + next = undo->next; + g_object_unref (undo->rule); + g_free (undo); + undo = next; + } + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_rule_editor_parent_class)->finalize (object); +} + +static void +rule_editor_dispose (GObject *object) +{ + ERuleEditor *editor = E_RULE_EDITOR (object); + + if (editor->dialog != NULL) { + gtk_widget_destroy (GTK_WIDGET (editor->dialog)); + editor->dialog = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_rule_editor_parent_class)->dispose (object); +} + +static void +rule_editor_set_source (ERuleEditor *editor, + const gchar *source) +{ + EFilterRule *rule = NULL; + GtkTreeIter iter; + + gtk_list_store_clear (editor->model); + + while ((rule = e_rule_context_next_rule (editor->context, rule, source)) != NULL) { + gtk_list_store_append (editor->model, &iter); + gtk_list_store_set ( + editor->model, &iter, + 0, rule->name, 1, rule, 2, rule->enabled, -1); + } + + g_free (editor->source); + editor->source = g_strdup (source); + editor->current = NULL; + e_rule_editor_set_sensitive (editor); +} + +static void +rule_editor_set_sensitive (ERuleEditor *editor) +{ + EFilterRule *rule = NULL; + gint index = -1, count = 0; + + while ((rule = e_rule_context_next_rule (editor->context, rule, editor->source))) { + if (rule == editor->current) + index = count; + count++; + } + + count--; + + gtk_widget_set_sensitive (GTK_WIDGET (editor->priv->buttons[BUTTON_EDIT]), index != -1); + gtk_widget_set_sensitive (GTK_WIDGET (editor->priv->buttons[BUTTON_DELETE]), index != -1); + gtk_widget_set_sensitive (GTK_WIDGET (editor->priv->buttons[BUTTON_TOP]), index > 0); + gtk_widget_set_sensitive (GTK_WIDGET (editor->priv->buttons[BUTTON_UP]), index > 0); + gtk_widget_set_sensitive (GTK_WIDGET (editor->priv->buttons[BUTTON_DOWN]), index >= 0 && index < count); + gtk_widget_set_sensitive (GTK_WIDGET (editor->priv->buttons[BUTTON_BOTTOM]), index >= 0 && index < count); +} + +static EFilterRule * +rule_editor_create_rule (ERuleEditor *editor) +{ + EFilterRule *rule; + EFilterPart *part; + + /* create a rule with 1 part in it */ + rule = e_filter_rule_new (); + part = e_rule_context_next_part (editor->context, NULL); + e_filter_rule_add_part (rule, e_filter_part_clone (part)); + + return rule; +} + +static void +e_rule_editor_class_init (ERuleEditorClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ERuleEditorPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = rule_editor_finalize; + object_class->dispose = rule_editor_dispose; + + class->set_source = rule_editor_set_source; + class->set_sensitive = rule_editor_set_sensitive; + class->create_rule = rule_editor_create_rule; + + /* TODO: Remove when it works (or never will) */ + enable_undo = getenv ("EVOLUTION_RULE_UNDO") != NULL; +} + +static void +e_rule_editor_init (ERuleEditor *editor) +{ + editor->priv = E_RULE_EDITOR_GET_PRIVATE (editor); +} + +/** + * rule_editor_new: + * + * Create a new ERuleEditor object. + * + * Return value: A new #ERuleEditor object. + **/ +ERuleEditor * +e_rule_editor_new (ERuleContext *context, + const gchar *source, + const gchar *label) +{ + ERuleEditor *editor = (ERuleEditor *) g_object_new (E_TYPE_RULE_EDITOR, NULL); + GtkBuilder *builder; + + builder = gtk_builder_new (); + e_load_ui_builder_definition (builder, "filter.ui"); + e_rule_editor_construct (editor, context, builder, source, label); + gtk_widget_hide (e_builder_get_widget (builder, "label17")); + gtk_widget_hide (e_builder_get_widget (builder, "filter_source_combobox")); + g_object_unref (builder); + + return editor; +} + +void +e_rule_editor_set_sensitive (ERuleEditor *editor) +{ + ERuleEditorClass *class; + + g_return_if_fail (E_IS_RULE_EDITOR (editor)); + + class = E_RULE_EDITOR_GET_CLASS (editor); + g_return_if_fail (class->set_sensitive != NULL); + + class->set_sensitive (editor); +} + +void +e_rule_editor_set_source (ERuleEditor *editor, + const gchar *source) +{ + ERuleEditorClass *class; + + g_return_if_fail (E_IS_RULE_EDITOR (editor)); + + class = E_RULE_EDITOR_GET_CLASS (editor); + g_return_if_fail (class->set_source != NULL); + + class->set_source (editor, source); +} + +EFilterRule * +e_rule_editor_create_rule (ERuleEditor *editor) +{ + ERuleEditorClass *class; + + g_return_val_if_fail (E_IS_RULE_EDITOR (editor), NULL); + + class = E_RULE_EDITOR_GET_CLASS (editor); + g_return_val_if_fail (class->create_rule != NULL, NULL); + + return class->create_rule (editor); +} + +static void +double_click (GtkTreeView *treeview, + GtkTreePath *path, + GtkTreeViewColumn *column, + ERuleEditor *editor) +{ + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreeIter iter; + + selection = gtk_tree_view_get_selection (editor->list); + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + gtk_tree_model_get (GTK_TREE_MODEL (editor->model), &iter, 1, &editor->current, -1); + + if (editor->current) + rule_edit ((GtkWidget *) treeview, editor); +} + +static void +rule_able_toggled (GtkCellRendererToggle *renderer, + gchar *path_string, + gpointer user_data) +{ + GtkWidget *table = user_data; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + + path = gtk_tree_path_new_from_string (path_string); + model = gtk_tree_view_get_model (GTK_TREE_VIEW (table)); + + if (gtk_tree_model_get_iter (model, &iter, path)) { + EFilterRule *rule = NULL; + + gtk_tree_model_get (model, &iter, 1, &rule, -1); + + if (rule) { + rule->enabled = !rule->enabled; + gtk_list_store_set (GTK_LIST_STORE (model), &iter, 2, rule->enabled, -1); + } + } + + gtk_tree_path_free (path); +} + +void +e_rule_editor_construct (ERuleEditor *editor, + ERuleContext *context, + GtkBuilder *builder, + const gchar *source, + const gchar *label) +{ + GtkWidget *widget; + GtkWidget *action_area; + GtkWidget *content_area; + GtkTreeViewColumn *column; + GtkCellRenderer *renderer; + GtkTreeSelection *selection; + GObject *object; + GList *list; + gint i; + + g_return_if_fail (E_IS_RULE_EDITOR (editor)); + g_return_if_fail (E_IS_RULE_CONTEXT (context)); + g_return_if_fail (GTK_IS_BUILDER (builder)); + + editor->context = g_object_ref (context); + + action_area = gtk_dialog_get_action_area (GTK_DIALOG (editor)); + content_area = gtk_dialog_get_content_area (GTK_DIALOG (editor)); + + gtk_window_set_resizable ((GtkWindow *) editor, TRUE); + gtk_window_set_default_size ((GtkWindow *) editor, 350, 400); + gtk_widget_realize ((GtkWidget *) editor); + gtk_container_set_border_width (GTK_CONTAINER (action_area), 12); + + widget = e_builder_get_widget (builder, "rule_editor"); + gtk_box_pack_start (GTK_BOX (content_area), widget, TRUE, TRUE, 0); + + for (i = 0; i < BUTTON_LAST; i++) { + widget = e_builder_get_widget (builder, edit_buttons[i].name); + editor->priv->buttons[i] = GTK_BUTTON (widget); + g_signal_connect ( + widget, "clicked", + G_CALLBACK (edit_buttons[i].func), editor); + } + + object = gtk_builder_get_object (builder, "rule_tree_view"); + editor->list = GTK_TREE_VIEW (object); + + column = gtk_tree_view_get_column (GTK_TREE_VIEW (object), 0); + g_return_if_fail (column != NULL); + + gtk_tree_view_column_set_visible (column, FALSE); + list = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (column)); + g_return_if_fail (list != NULL); + + renderer = GTK_CELL_RENDERER (list->data); + g_warn_if_fail (GTK_IS_CELL_RENDERER_TOGGLE (renderer)); + + g_signal_connect ( + renderer, "toggled", + G_CALLBACK (rule_able_toggled), editor->list); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (object)); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); + + object = gtk_builder_get_object (builder, "rule_list_store"); + editor->model = GTK_LIST_STORE (object); + + g_signal_connect ( + editor->list, "cursor-changed", + G_CALLBACK (cursor_changed), editor); + g_signal_connect ( + editor->list, "row-activated", + G_CALLBACK (double_click), editor); + + widget = e_builder_get_widget (builder, "rule_label"); + gtk_label_set_label (GTK_LABEL (widget), label); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), GTK_WIDGET (editor->list)); + + g_signal_connect ( + editor, "response", + G_CALLBACK (editor_response), editor); + rule_editor_set_source (editor, source); + + gtk_dialog_add_buttons ( + GTK_DIALOG (editor), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, + NULL); +} diff --git a/e-util/e-rule-editor.h b/e-util/e-rule-editor.h new file mode 100644 index 0000000000..d983b81c27 --- /dev/null +++ b/e-util/e-rule-editor.h @@ -0,0 +1,125 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Not Zed <notzed@lostzed.mmc.com.au> + * Jeffrey Stedfast <fejj@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_RULE_EDITOR_H +#define E_RULE_EDITOR_H + +#include <gtk/gtk.h> + +#include <e-util/e-rule-context.h> +#include <e-util/e-filter-rule.h> + +/* Standard GObject macros */ +#define E_TYPE_RULE_EDITOR \ + (e_rule_editor_get_type ()) +#define E_RULE_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_RULE_EDITOR, ERuleEditor)) +#define E_RULE_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_RULE_EDITOR, ERuleEditorClass)) +#define E_IS_RULE_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_RULE_EDITOR)) +#define E_IS_RULE_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_RULE_EDITOR)) +#define E_RULE_EDITOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_RULE_EDITOR, ERuleEditorClass)) + +G_BEGIN_DECLS + +typedef struct _ERuleEditor ERuleEditor; +typedef struct _ERuleEditorClass ERuleEditorClass; +typedef struct _ERuleEditorPrivate ERuleEditorPrivate; + +typedef struct _ERuleEditorUndo ERuleEditorUndo; + +struct _ERuleEditor { + GtkDialog parent; + + GtkListStore *model; + GtkTreeView *list; + + ERuleContext *context; + EFilterRule *current; + EFilterRule *edit; /* for editing/adding rules, so we only do 1 at a time */ + + GtkWidget *dialog; + + gchar *source; + + ERuleEditorUndo *undo_log; /* cancel/undo log */ + guint undo_active:1; /* we're performing undo */ + + ERuleEditorPrivate *priv; +}; + +struct _ERuleEditorClass { + GtkDialogClass parent_class; + + void (*set_sensitive) (ERuleEditor *editor); + void (*set_source) (ERuleEditor *editor, + const gchar *source); + + EFilterRule * (*create_rule) (ERuleEditor *editor); +}; + +enum { + E_RULE_EDITOR_LOG_EDIT, + E_RULE_EDITOR_LOG_ADD, + E_RULE_EDITOR_LOG_REMOVE, + E_RULE_EDITOR_LOG_RANK +}; + +struct _ERuleEditorUndo { + ERuleEditorUndo *next; + + guint type; + EFilterRule *rule; + gint rank; + gint newrank; +}; + +GType e_rule_editor_get_type (void); +ERuleEditor * e_rule_editor_new (ERuleContext *context, + const gchar *source, + const gchar *label); +void e_rule_editor_construct (ERuleEditor *editor, + ERuleContext *context, + GtkBuilder *builder, + const gchar *source, + const gchar *label); +void e_rule_editor_set_source (ERuleEditor *editor, + const gchar *source); +void e_rule_editor_set_sensitive (ERuleEditor *editor); +EFilterRule * e_rule_editor_create_rule (ERuleEditor *editor); + +G_END_DECLS + +#endif /* E_RULE_EDITOR_H */ diff --git a/e-util/e-search-bar.c b/e-util/e-search-bar.c new file mode 100644 index 0000000000..9ed0c2d1c9 --- /dev/null +++ b/e-util/e-search-bar.c @@ -0,0 +1,771 @@ +/* + * e-search-bar.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-search-bar.h" + +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#define E_SEARCH_BAR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SEARCH_BAR, ESearchBarPrivate)) + +struct _ESearchBarPrivate { + EWebView *web_view; + GtkWidget *entry; + GtkWidget *case_sensitive_button; + GtkWidget *wrapped_next_box; + GtkWidget *wrapped_prev_box; + GtkWidget *matches_label; + + gchar *active_search; + + guint rerun_search : 1; +}; + +enum { + PROP_0, + PROP_ACTIVE_SEARCH, + PROP_CASE_SENSITIVE, + PROP_TEXT, + PROP_WEB_VIEW +}; + +enum { + CHANGED, + CLEAR, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE ( + ESearchBar, + e_search_bar, + GTK_TYPE_HBOX) + +static void +search_bar_update_matches (ESearchBar *search_bar, + guint matches) +{ + GtkWidget *matches_label; + gchar *text; + + search_bar->priv->rerun_search = FALSE; + matches_label = search_bar->priv->matches_label; + + text = g_strdup_printf (_("Matches: %u"), matches); + gtk_label_set_text (GTK_LABEL (matches_label), text); + gtk_widget_show (matches_label); + g_free (text); +} + +static void +search_bar_update_highlights (ESearchBar *search_bar) +{ + EWebView *web_view; + gboolean visible; + + web_view = e_search_bar_get_web_view (search_bar); + + visible = gtk_widget_get_visible (GTK_WIDGET (search_bar)); + + webkit_web_view_set_highlight_text_matches ( + WEBKIT_WEB_VIEW (web_view), visible); + + e_search_bar_changed (search_bar); +} + +static void +search_bar_find (ESearchBar *search_bar, + gboolean search_forward) +{ + EWebView *web_view; + GtkWidget *widget; + gboolean case_sensitive; + gboolean new_search; + gboolean wrapped = FALSE; + gboolean success; + gchar *text; + + web_view = e_search_bar_get_web_view (search_bar); + case_sensitive = e_search_bar_get_case_sensitive (search_bar); + text = e_search_bar_get_text (search_bar); + + if (text == NULL || *text == '\0') { + e_search_bar_clear (search_bar); + g_free (text); + return; + } + + new_search = + (search_bar->priv->active_search == NULL) || + (g_strcmp0 (text, search_bar->priv->active_search) != 0); + + if (new_search) { + guint matches; + + webkit_web_view_unmark_text_matches ( + WEBKIT_WEB_VIEW (web_view)); + matches = webkit_web_view_mark_text_matches ( + WEBKIT_WEB_VIEW (web_view), + text, case_sensitive, 0); + webkit_web_view_set_highlight_text_matches ( + WEBKIT_WEB_VIEW (web_view), TRUE); + search_bar_update_matches (search_bar, matches); + } + + success = webkit_web_view_search_text ( + WEBKIT_WEB_VIEW (web_view), + text, case_sensitive, search_forward, FALSE); + + if (!success) + wrapped = webkit_web_view_search_text ( + WEBKIT_WEB_VIEW (web_view), + text, case_sensitive, search_forward, TRUE); + + g_free (search_bar->priv->active_search); + search_bar->priv->active_search = text; + + g_object_notify (G_OBJECT (search_bar), "active-search"); + + /* Update wrapped label visibility. */ + + widget = search_bar->priv->wrapped_next_box; + + if (wrapped && search_forward) + gtk_widget_show (widget); + else + gtk_widget_hide (widget); + + widget = search_bar->priv->wrapped_prev_box; + + if (wrapped && !search_forward) + gtk_widget_show (widget); + else + gtk_widget_hide (widget); +} + +static void +search_bar_changed_cb (ESearchBar *search_bar) +{ + g_object_notify (G_OBJECT (search_bar), "text"); +} + +static void +search_bar_find_next_cb (ESearchBar *search_bar) +{ + search_bar_find (search_bar, TRUE); +} + +static void +search_bar_find_previous_cb (ESearchBar *search_bar) +{ + search_bar_find (search_bar, FALSE); +} + +static void +search_bar_icon_release_cb (ESearchBar *search_bar, + GtkEntryIconPosition icon_pos, + GdkEvent *event) +{ + g_return_if_fail (icon_pos == GTK_ENTRY_ICON_SECONDARY); + + e_search_bar_clear (search_bar); + gtk_widget_grab_focus (search_bar->priv->entry); +} + +static void +search_bar_toggled_cb (ESearchBar *search_bar) +{ + g_free (search_bar->priv->active_search); + search_bar->priv->active_search = NULL; + + g_object_notify (G_OBJECT (search_bar), "active-search"); + g_object_notify (G_OBJECT (search_bar), "case-sensitive"); +} + +static void +search_bar_set_web_view (ESearchBar *search_bar, + EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + g_return_if_fail (search_bar->priv->web_view == NULL); + + search_bar->priv->web_view = g_object_ref (web_view); +} + +static void +search_bar_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CASE_SENSITIVE: + e_search_bar_set_case_sensitive ( + E_SEARCH_BAR (object), + g_value_get_boolean (value)); + return; + + case PROP_TEXT: + e_search_bar_set_text ( + E_SEARCH_BAR (object), + g_value_get_string (value)); + return; + + case PROP_WEB_VIEW: + search_bar_set_web_view ( + E_SEARCH_BAR (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +search_bar_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVE_SEARCH: + g_value_set_boolean ( + value, e_search_bar_get_active_search ( + E_SEARCH_BAR (object))); + return; + + case PROP_CASE_SENSITIVE: + g_value_set_boolean ( + value, e_search_bar_get_case_sensitive ( + E_SEARCH_BAR (object))); + return; + + case PROP_TEXT: + g_value_take_string ( + value, e_search_bar_get_text ( + E_SEARCH_BAR (object))); + return; + + case PROP_WEB_VIEW: + g_value_set_object ( + value, e_search_bar_get_web_view ( + E_SEARCH_BAR (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +search_bar_dispose (GObject *object) +{ + ESearchBarPrivate *priv; + + priv = E_SEARCH_BAR_GET_PRIVATE (object); + + if (priv->web_view != NULL) { + g_object_unref (priv->web_view); + priv->web_view = NULL; + } + + if (priv->entry != NULL) { + g_object_unref (priv->entry); + priv->entry = NULL; + } + + if (priv->case_sensitive_button != NULL) { + g_object_unref (priv->case_sensitive_button); + priv->case_sensitive_button = NULL; + } + + if (priv->wrapped_next_box != NULL) { + g_object_unref (priv->wrapped_next_box); + priv->wrapped_next_box = NULL; + } + + if (priv->wrapped_prev_box != NULL) { + g_object_unref (priv->wrapped_prev_box); + priv->wrapped_prev_box = NULL; + } + + if (priv->matches_label != NULL) { + g_object_unref (priv->matches_label); + priv->matches_label = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_search_bar_parent_class)->dispose (object); +} + +static void +search_bar_finalize (GObject *object) +{ + ESearchBarPrivate *priv; + + priv = E_SEARCH_BAR_GET_PRIVATE (object); + + g_free (priv->active_search); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_search_bar_parent_class)->finalize (object); +} + +static void +search_bar_constructed (GObject *object) +{ + ESearchBarPrivate *priv; + + priv = E_SEARCH_BAR_GET_PRIVATE (object); + + g_object_bind_property ( + object, "case-sensitive", + priv->case_sensitive_button, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_search_bar_parent_class)->constructed (object); +} + +static void +search_bar_show (GtkWidget *widget) +{ + ESearchBar *search_bar; + + search_bar = E_SEARCH_BAR (widget); + + /* Chain up to parent's show() method. */ + GTK_WIDGET_CLASS (e_search_bar_parent_class)->show (widget); + + gtk_widget_grab_focus (search_bar->priv->entry); + + search_bar_update_highlights (search_bar); +} + +static void +search_bar_hide (GtkWidget *widget) +{ + ESearchBar *search_bar; + + search_bar = E_SEARCH_BAR (widget); + + /* Chain up to parent's hide() method. */ + GTK_WIDGET_CLASS (e_search_bar_parent_class)->hide (widget); + + search_bar_update_highlights (search_bar); +} + +static gboolean +search_bar_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + GtkWidgetClass *widget_class; + + if (event->keyval == GDK_KEY_Escape) { + gtk_widget_hide (widget); + return TRUE; + } + + /* Chain up to parent's key_press_event() method. */ + widget_class = GTK_WIDGET_CLASS (e_search_bar_parent_class); + return widget_class->key_press_event (widget, event); +} + +static void +search_bar_clear (ESearchBar *search_bar) +{ + WebKitWebView *web_view; + + g_free (search_bar->priv->active_search); + search_bar->priv->active_search = NULL; + + gtk_entry_set_text (GTK_ENTRY (search_bar->priv->entry), ""); + + gtk_widget_hide (search_bar->priv->wrapped_next_box); + gtk_widget_hide (search_bar->priv->wrapped_prev_box); + gtk_widget_hide (search_bar->priv->matches_label); + + search_bar_update_highlights (search_bar); + + web_view = WEBKIT_WEB_VIEW (search_bar->priv->web_view); + webkit_web_view_unmark_text_matches (web_view); + + g_object_notify (G_OBJECT (search_bar), "active-search"); +} + +static void +e_search_bar_class_init (ESearchBarClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (ESearchBarPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = search_bar_set_property; + object_class->get_property = search_bar_get_property; + object_class->dispose = search_bar_dispose; + object_class->finalize = search_bar_finalize; + object_class->constructed = search_bar_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = search_bar_show; + widget_class->hide = search_bar_hide; + widget_class->key_press_event = search_bar_key_press_event; + + class->clear = search_bar_clear; + + g_object_class_install_property ( + object_class, + PROP_ACTIVE_SEARCH, + g_param_spec_boolean ( + "active-search", + "Active Search", + NULL, + FALSE, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_CASE_SENSITIVE, + g_param_spec_boolean ( + "case-sensitive", + "Case Sensitive", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_TEXT, + g_param_spec_string ( + "text", + "Search Text", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WEB_VIEW, + g_param_spec_object ( + "web-view", + "Web View", + NULL, + E_TYPE_WEB_VIEW, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); + + signals[CHANGED] = g_signal_new ( + "changed", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (ESearchBarClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[CLEAR] = g_signal_new ( + "clear", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (ESearchBarClass, clear), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_search_bar_init (ESearchBar *search_bar) +{ + GtkWidget *label; + GtkWidget *widget; + GtkWidget *container; + + search_bar->priv = E_SEARCH_BAR_GET_PRIVATE (search_bar); + + gtk_box_set_spacing (GTK_BOX (search_bar), 12); + + container = GTK_WIDGET (search_bar); + + widget = gtk_hbox_new (FALSE, 1); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_button_new (); + gtk_button_set_image ( + GTK_BUTTON (widget), gtk_image_new_from_stock ( + GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU)); + gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text (widget, _("Close the find bar")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (gtk_widget_hide), search_bar); + + widget = gtk_label_new_with_mnemonic (_("Fin_d:")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 3); + gtk_widget_show (widget); + + label = widget; + + widget = gtk_entry_new (); + gtk_entry_set_icon_from_stock ( + GTK_ENTRY (widget), GTK_ENTRY_ICON_SECONDARY, + GTK_STOCK_CLEAR); + gtk_entry_set_icon_tooltip_text ( + GTK_ENTRY (widget), GTK_ENTRY_ICON_SECONDARY, + _("Clear the search")); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget); + gtk_widget_set_size_request (widget, 200, -1); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + search_bar->priv->entry = g_object_ref (widget); + gtk_widget_show (widget); + + g_object_bind_property ( + search_bar, "active-search", + widget, "secondary-icon-sensitive", + G_BINDING_SYNC_CREATE); + + g_signal_connect_swapped ( + widget, "activate", + G_CALLBACK (search_bar_find_next_cb), search_bar); + + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (search_bar_changed_cb), search_bar); + + g_signal_connect_swapped ( + widget, "icon-release", + G_CALLBACK (search_bar_icon_release_cb), search_bar); + + widget = gtk_button_new_with_mnemonic (_("_Previous")); + gtk_button_set_image ( + GTK_BUTTON (widget), gtk_image_new_from_stock ( + GTK_STOCK_GO_BACK, GTK_ICON_SIZE_MENU)); + gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text ( + widget, _("Find the previous occurrence of the phrase")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + g_object_bind_property ( + search_bar, "active-search", + widget, "sensitive", + G_BINDING_SYNC_CREATE); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (search_bar_find_previous_cb), search_bar); + + widget = gtk_button_new_with_mnemonic (_("_Next")); + gtk_button_set_image ( + GTK_BUTTON (widget), gtk_image_new_from_stock ( + GTK_STOCK_GO_FORWARD, GTK_ICON_SIZE_MENU)); + gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text ( + widget, _("Find the next occurrence of the phrase")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + g_object_bind_property ( + search_bar, "active-search", + widget, "sensitive", + G_BINDING_SYNC_CREATE); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (search_bar_find_next_cb), search_bar); + + widget = gtk_check_button_new_with_mnemonic (_("Mat_ch case")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + search_bar->priv->case_sensitive_button = g_object_ref (widget); + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (search_bar_toggled_cb), search_bar); + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (search_bar_find_next_cb), search_bar); + + container = GTK_WIDGET (search_bar); + + widget = gtk_hbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + search_bar->priv->wrapped_next_box = g_object_ref (widget); + gtk_widget_hide (widget); + + container = widget; + + widget = gtk_image_new_from_icon_name ( + "wrapped", GTK_ICON_SIZE_MENU); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_label_new ( + _("Reached bottom of page, continued from top")); + gtk_label_set_ellipsize ( + GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = GTK_WIDGET (search_bar); + + widget = gtk_hbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + search_bar->priv->wrapped_prev_box = g_object_ref (widget); + gtk_widget_hide (widget); + + container = widget; + + widget = gtk_image_new_from_icon_name ( + "wrapped", GTK_ICON_SIZE_MENU); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_label_new ( + _("Reached top of page, continued from bottom")); + gtk_label_set_ellipsize ( + GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = GTK_WIDGET (search_bar); + + widget = gtk_label_new (NULL); + gtk_box_pack_end (GTK_BOX (container), widget, FALSE, FALSE, 12); + search_bar->priv->matches_label = g_object_ref (widget); + gtk_widget_show (widget); +} + +GtkWidget * +e_search_bar_new (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return g_object_new ( + E_TYPE_SEARCH_BAR, "web-view", web_view, NULL); +} + +void +e_search_bar_clear (ESearchBar *search_bar) +{ + g_return_if_fail (E_IS_SEARCH_BAR (search_bar)); + + g_signal_emit (search_bar, signals[CLEAR], 0); +} + +void +e_search_bar_changed (ESearchBar *search_bar) +{ + g_return_if_fail (E_IS_SEARCH_BAR (search_bar)); + + g_signal_emit (search_bar, signals[CHANGED], 0); +} + +EWebView * +e_search_bar_get_web_view (ESearchBar *search_bar) +{ + g_return_val_if_fail (E_IS_SEARCH_BAR (search_bar), NULL); + + return search_bar->priv->web_view; +} + +gboolean +e_search_bar_get_active_search (ESearchBar *search_bar) +{ + g_return_val_if_fail (E_IS_SEARCH_BAR (search_bar), FALSE); + + return (search_bar->priv->active_search != NULL); +} + +gboolean +e_search_bar_get_case_sensitive (ESearchBar *search_bar) +{ + GtkToggleButton *button; + + g_return_val_if_fail (E_IS_SEARCH_BAR (search_bar), FALSE); + + button = GTK_TOGGLE_BUTTON (search_bar->priv->case_sensitive_button); + + return gtk_toggle_button_get_active (button); +} + +void +e_search_bar_set_case_sensitive (ESearchBar *search_bar, + gboolean case_sensitive) +{ + GtkToggleButton *button; + + g_return_if_fail (E_IS_SEARCH_BAR (search_bar)); + + button = GTK_TOGGLE_BUTTON (search_bar->priv->case_sensitive_button); + + gtk_toggle_button_set_active (button, case_sensitive); + + g_object_notify (G_OBJECT (search_bar), "case-sensitive"); +} + +gchar * +e_search_bar_get_text (ESearchBar *search_bar) +{ + GtkEntry *entry; + const gchar *text; + + g_return_val_if_fail (E_IS_SEARCH_BAR (search_bar), NULL); + + entry = GTK_ENTRY (search_bar->priv->entry); + text = gtk_entry_get_text (entry); + + return g_strstrip (g_strdup (text)); +} + +void +e_search_bar_set_text (ESearchBar *search_bar, + const gchar *text) +{ + GtkEntry *entry; + + g_return_if_fail (E_IS_SEARCH_BAR (search_bar)); + + entry = GTK_ENTRY (search_bar->priv->entry); + + if (text == NULL) + text = ""; + + /* This will trigger a "notify::text" signal. */ + gtk_entry_set_text (entry, text); +} diff --git a/e-util/e-search-bar.h b/e-util/e-search-bar.h new file mode 100644 index 0000000000..43e16453bd --- /dev/null +++ b/e-util/e-search-bar.h @@ -0,0 +1,89 @@ +/* + * e-search-bar.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SEARCH_BAR_H +#define E_SEARCH_BAR_H + +#include <gtk/gtk.h> + +#include <e-util/e-web-view.h> + +/* Standard GObject macros */ +#define E_TYPE_SEARCH_BAR \ + (e_search_bar_get_type ()) +#define E_SEARCH_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SEARCH_BAR, ESearchBar)) +#define E_SEARCH_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SEARCH_BAR, ESearchBarClass)) +#define E_IS_SEARCH_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SEARCH_BAR)) +#define E_IS_SEARCH_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SEARCH_BAR)) +#define E_SEARCH_BAR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SEARCH_BAR, ESearchBarClass)) + +G_BEGIN_DECLS + +typedef struct _ESearchBar ESearchBar; +typedef struct _ESearchBarClass ESearchBarClass; +typedef struct _ESearchBarPrivate ESearchBarPrivate; + +struct _ESearchBar { + GtkBox parent; + ESearchBarPrivate *priv; +}; + +struct _ESearchBarClass { + GtkBoxClass parent_class; + + /* Signals */ + void (*changed) (ESearchBar *search_bar); + void (*clear) (ESearchBar *search_bar); +}; + +GType e_search_bar_get_type (void); +GtkWidget * e_search_bar_new (EWebView *web_view); +void e_search_bar_clear (ESearchBar *search_bar); +void e_search_bar_changed (ESearchBar *search_bar); +EWebView * e_search_bar_get_web_view (ESearchBar *search_bar); +gboolean e_search_bar_get_active_search + (ESearchBar *search_bar); +gboolean e_search_bar_get_case_sensitive + (ESearchBar *search_bar); +void e_search_bar_set_case_sensitive + (ESearchBar *search_bar, + gboolean case_sensitive); +gchar * e_search_bar_get_text (ESearchBar *search_bar); +void e_search_bar_set_text (ESearchBar *search_bar, + const gchar *text); + +G_END_DECLS + +#endif /* E_SEARCH_BAR_H */ diff --git a/e-util/e-selectable.c b/e-util/e-selectable.c new file mode 100644 index 0000000000..b8e4337fef --- /dev/null +++ b/e-util/e-selectable.c @@ -0,0 +1,168 @@ +/* + * e-selectable.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-selectable.h" + +G_DEFINE_INTERFACE ( + ESelectable, + e_selectable, + GTK_TYPE_WIDGET) + +static void +e_selectable_default_init (ESelectableInterface *interface) +{ + g_object_interface_install_property ( + interface, + g_param_spec_boxed ( + "copy-target-list", + "Copy Target List", + NULL, + GTK_TYPE_TARGET_LIST, + G_PARAM_READABLE)); + + g_object_interface_install_property ( + interface, + g_param_spec_boxed ( + "paste-target-list", + "Paste Target List", + NULL, + GTK_TYPE_TARGET_LIST, + G_PARAM_READABLE)); +} + +void +e_selectable_update_actions (ESelectable *selectable, + EFocusTracker *focus_tracker, + GdkAtom *clipboard_targets, + gint n_clipboard_targets) +{ + ESelectableInterface *interface; + + g_return_if_fail (E_IS_SELECTABLE (selectable)); + + interface = E_SELECTABLE_GET_INTERFACE (selectable); + g_return_if_fail (interface->update_actions != NULL); + + interface->update_actions ( + selectable, focus_tracker, + clipboard_targets, n_clipboard_targets); +} + +void +e_selectable_cut_clipboard (ESelectable *selectable) +{ + ESelectableInterface *interface; + + g_return_if_fail (E_IS_SELECTABLE (selectable)); + + interface = E_SELECTABLE_GET_INTERFACE (selectable); + + if (interface->cut_clipboard != NULL) + interface->cut_clipboard (selectable); +} + +void +e_selectable_copy_clipboard (ESelectable *selectable) +{ + ESelectableInterface *interface; + + g_return_if_fail (E_IS_SELECTABLE (selectable)); + + interface = E_SELECTABLE_GET_INTERFACE (selectable); + + if (interface->copy_clipboard != NULL) + interface->copy_clipboard (selectable); +} + +void +e_selectable_paste_clipboard (ESelectable *selectable) +{ + ESelectableInterface *interface; + + g_return_if_fail (E_IS_SELECTABLE (selectable)); + + interface = E_SELECTABLE_GET_INTERFACE (selectable); + + if (interface->paste_clipboard != NULL) + interface->paste_clipboard (selectable); +} + +void +e_selectable_delete_selection (ESelectable *selectable) +{ + ESelectableInterface *interface; + + g_return_if_fail (E_IS_SELECTABLE (selectable)); + + interface = E_SELECTABLE_GET_INTERFACE (selectable); + + if (interface->delete_selection != NULL) + interface->delete_selection (selectable); +} + +void +e_selectable_select_all (ESelectable *selectable) +{ + ESelectableInterface *interface; + + g_return_if_fail (E_IS_SELECTABLE (selectable)); + + interface = E_SELECTABLE_GET_INTERFACE (selectable); + + if (interface->select_all != NULL) + interface->select_all (selectable); +} + +GtkTargetList * +e_selectable_get_copy_target_list (ESelectable *selectable) +{ + GtkTargetList *target_list; + + g_return_val_if_fail (E_IS_SELECTABLE (selectable), NULL); + + g_object_get (selectable, "copy-target-list", &target_list, NULL); + + /* We want to return a borrowed reference to the target + * list, so undo the reference that g_object_get() added. */ + gtk_target_list_unref (target_list); + + return target_list; +} + +GtkTargetList * +e_selectable_get_paste_target_list (ESelectable *selectable) +{ + GtkTargetList *target_list; + + g_return_val_if_fail (E_IS_SELECTABLE (selectable), NULL); + + g_object_get (selectable, "paste-target-list", &target_list, NULL); + + /* We want to return a borrowed reference to the target + * list, so undo the reference that g_object_get() added. */ + gtk_target_list_unref (target_list); + + return target_list; +} diff --git a/e-util/e-selectable.h b/e-util/e-selectable.h new file mode 100644 index 0000000000..4e7faa8581 --- /dev/null +++ b/e-util/e-selectable.h @@ -0,0 +1,85 @@ +/* + * e-selectable.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SELECTABLE_H +#define E_SELECTABLE_H + +#include <gtk/gtk.h> + +#include <e-util/e-focus-tracker.h> + +/* Standard GObject macros */ +#define E_TYPE_SELECTABLE \ + (e_selectable_get_type ()) +#define E_SELECTABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SELECTABLE, ESelectable)) +#define E_IS_SELECTABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SELECTABLE)) +#define E_SELECTABLE_GET_INTERFACE(obj) \ + (G_TYPE_INSTANCE_GET_INTERFACE \ + ((obj), E_TYPE_SELECTABLE, ESelectableInterface)) + +G_BEGIN_DECLS + +typedef struct _ESelectable ESelectable; +typedef struct _ESelectableInterface ESelectableInterface; + +struct _ESelectableInterface { + GTypeInterface parent_iface; + + /* Required Methods */ + void (*update_actions) (ESelectable *selectable, + EFocusTracker *focus_tracker, + GdkAtom *clipboard_targets, + gint n_clipboard_targets); + + /* Optional Methods */ + void (*cut_clipboard) (ESelectable *selectable); + void (*copy_clipboard) (ESelectable *selectable); + void (*paste_clipboard) (ESelectable *selectable); + void (*delete_selection) (ESelectable *selectable); + void (*select_all) (ESelectable *selectable); +}; + +GType e_selectable_get_type (void); +void e_selectable_update_actions (ESelectable *selectable, + EFocusTracker *focus_tracker, + GdkAtom *clipboard_targets, + gint n_clipboard_targets); +void e_selectable_cut_clipboard (ESelectable *selectable); +void e_selectable_copy_clipboard (ESelectable *selectable); +void e_selectable_paste_clipboard (ESelectable *selectable); +void e_selectable_delete_selection (ESelectable *selectable); +void e_selectable_select_all (ESelectable *selectable); +GtkTargetList * e_selectable_get_copy_target_list + (ESelectable *selectable); +GtkTargetList * e_selectable_get_paste_target_list + (ESelectable *selectable); + +G_END_DECLS + +#endif /* E_SELECTABLE_H */ diff --git a/e-util/e-selection-model-array.c b/e-util/e-selection-model-array.c new file mode 100644 index 0000000000..fe73857a8a --- /dev/null +++ b/e-util/e-selection-model-array.c @@ -0,0 +1,646 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> + +#include <glib/gi18n.h> + +#include "e-selection-model-array.h" + +G_DEFINE_TYPE ( + ESelectionModelArray, + e_selection_model_array, + E_TYPE_SELECTION_MODEL) + +enum { + PROP_0, + PROP_CURSOR_ROW, + PROP_CURSOR_COL +}; + +void +e_selection_model_array_confirm_row_count (ESelectionModelArray *esma) +{ + if (esma->eba == NULL) { + gint row_count = e_selection_model_array_get_row_count (esma); + esma->eba = e_bit_array_new (row_count); + esma->selected_row = -1; + esma->selected_range_end = -1; + } +} + +static gint +es_row_model_to_sorted (ESelectionModelArray *esma, + gint model_row) +{ + if (model_row >= 0 && esma && esma->base.sorter && e_sorter_needs_sorting (esma->base.sorter)) + return e_sorter_model_to_sorted (esma->base.sorter, model_row); + + return model_row; +} + +static gint +es_row_sorted_to_model (ESelectionModelArray *esma, + gint sorted_row) +{ + if (sorted_row >= 0 && esma && esma->base.sorter && e_sorter_needs_sorting (esma->base.sorter)) + return e_sorter_sorted_to_model (esma->base.sorter, sorted_row); + + return sorted_row; +} + +/* FIXME: Should this deal with moving the selection if it's in single mode? */ +void +e_selection_model_array_delete_rows (ESelectionModelArray *esma, + gint row, + gint count) +{ + if (esma->eba) { + if (E_SELECTION_MODEL (esma)->mode == GTK_SELECTION_SINGLE) + e_bit_array_delete_single_mode (esma->eba, row, count); + else + e_bit_array_delete (esma->eba, row, count); + + if (esma->cursor_row >= row && esma->cursor_row < row + count) { + /* we should move the cursor_row, because some lines before us are going to be removed */ + if (esma->cursor_row_sorted >= e_bit_array_bit_count (esma->eba)) { + esma->cursor_row_sorted = e_bit_array_bit_count (esma->eba) - 1; + } + + if (esma->cursor_row_sorted >= 0) { + esma->cursor_row = es_row_sorted_to_model (esma, esma->cursor_row_sorted); + esma->selection_start_row = 0; + e_bit_array_change_one_row (esma->eba, esma->cursor_row, TRUE); + } else { + esma->cursor_row = -1; + esma->cursor_row_sorted = -1; + esma->selection_start_row = 0; + } + } else { + /* some code earlier changed the selected row, so just update the sorted one */ + if (esma->cursor_row >= row) + esma->cursor_row = MAX (0, esma->cursor_row - count); + + if (esma->cursor_row >= e_bit_array_bit_count (esma->eba)) + esma->cursor_row = e_bit_array_bit_count (esma->eba) - 1; + + if (esma->cursor_row >= 0) { + esma->cursor_row_sorted = es_row_model_to_sorted (esma, esma->cursor_row); + esma->selection_start_row = 0; + e_bit_array_change_one_row (esma->eba, esma->cursor_row, TRUE); + } else { + esma->cursor_row = -1; + esma->cursor_row_sorted = -1; + esma->selection_start_row = 0; + } + } + + esma->selected_row = -1; + esma->selected_range_end = -1; + e_selection_model_selection_changed (E_SELECTION_MODEL (esma)); + e_selection_model_cursor_changed (E_SELECTION_MODEL (esma), esma->cursor_row, esma->cursor_col); + } +} + +void +e_selection_model_array_insert_rows (ESelectionModelArray *esma, + gint row, + gint count) +{ + if (esma->eba) { + e_bit_array_insert (esma->eba, row, count); + + /* just recalculate new position of the previously set cursor row */ + esma->cursor_row = es_row_sorted_to_model (esma, esma->cursor_row_sorted); + + esma->selected_row = -1; + esma->selected_range_end = -1; + e_selection_model_selection_changed (E_SELECTION_MODEL (esma)); + e_selection_model_cursor_changed (E_SELECTION_MODEL (esma), esma->cursor_row, esma->cursor_col); + } +} + +void +e_selection_model_array_move_row (ESelectionModelArray *esma, + gint old_row, + gint new_row) +{ + ESelectionModel *esm = E_SELECTION_MODEL (esma); + + if (esma->eba) { + gboolean selected = e_bit_array_value_at (esma->eba, old_row); + gboolean cursor = (esma->cursor_row == old_row); + gint old_row_sorted, new_row_sorted; + + old_row_sorted = es_row_model_to_sorted (esma, old_row); + new_row_sorted = es_row_model_to_sorted (esma, new_row); + + if (old_row_sorted < esma->cursor_row_sorted && esma->cursor_row_sorted < new_row_sorted) + esma->cursor_row_sorted--; + else if (new_row_sorted < esma->cursor_row_sorted && esma->cursor_row_sorted < old_row_sorted) + esma->cursor_row_sorted++; + + e_bit_array_move_row (esma->eba, old_row, new_row); + + if (selected) { + if (esm->mode == GTK_SELECTION_SINGLE) + e_bit_array_select_single_row (esma->eba, new_row); + else + e_bit_array_change_one_row (esma->eba, new_row, TRUE); + } + if (cursor) { + esma->cursor_row = new_row; + esma->cursor_row_sorted = es_row_model_to_sorted (esma, esma->cursor_row); + } else + esma->cursor_row = es_row_sorted_to_model (esma, esma->cursor_row_sorted); + + esma->selected_row = -1; + esma->selected_range_end = -1; + e_selection_model_selection_changed (esm); + e_selection_model_cursor_changed (esm, esma->cursor_row, esma->cursor_col); + } +} + +static void +esma_dispose (GObject *object) +{ + ESelectionModelArray *esma; + + esma = E_SELECTION_MODEL_ARRAY (object); + + if (esma->eba) { + g_object_unref (esma->eba); + esma->eba = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_selection_model_array_parent_class)->dispose (object); +} + +static void +esma_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (object); + + switch (property_id) { + case PROP_CURSOR_ROW: + g_value_set_int (value, esma->cursor_row); + break; + + case PROP_CURSOR_COL: + g_value_set_int (value, esma->cursor_col); + break; + } +} + +static void +esma_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ESelectionModel *esm = E_SELECTION_MODEL (object); + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (object); + + switch (property_id) { + case PROP_CURSOR_ROW: + e_selection_model_do_something (esm, g_value_get_int (value), esma->cursor_col, 0); + break; + + case PROP_CURSOR_COL: + e_selection_model_do_something (esm, esma->cursor_row, g_value_get_int (value), 0); + break; + } +} + +/** + * e_selection_model_is_row_selected + * @selection: #ESelectionModel to check + * @n: The row to check + * + * This routine calculates whether the given row is selected. + * + * Returns: %TRUE if the given row is selected + */ +static gboolean +esma_is_row_selected (ESelectionModel *selection, + gint n) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + if (esma->eba) + return e_bit_array_value_at (esma->eba, n); + else + return FALSE; +} + +/** + * e_selection_model_foreach + * @selection: #ESelectionModel to traverse + * @callback: The callback function to call back. + * @closure: The closure + * + * This routine calls the given callback function once for each + * selected row, passing closure as the closure. + */ +static void +esma_foreach (ESelectionModel *selection, + EForeachFunc callback, + gpointer closure) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + if (esma->eba) + e_bit_array_foreach (esma->eba, callback, closure); +} + +/** + * e_selection_model_clear + * @selection: #ESelectionModel to clear + * + * This routine clears the selection to no rows selected. + */ +static void +esma_clear (ESelectionModel *selection) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + if (esma->eba) { + g_object_unref (esma->eba); + esma->eba = NULL; + } + esma->cursor_row = -1; + esma->cursor_col = -1; + esma->cursor_row_sorted = -1; + esma->selected_row = -1; + esma->selected_range_end = -1; + e_selection_model_selection_changed (E_SELECTION_MODEL (esma)); + e_selection_model_cursor_changed (E_SELECTION_MODEL (esma), -1, -1); +} + +#define PART(x,n) (((x) & (0x01010101 << n)) >> n) +#define SECTION(x, n) (((x) >> (n * 8)) & 0xff) + +/** + * e_selection_model_selected_count + * @selection: #ESelectionModel to count + * + * This routine calculates the number of rows selected. + * + * Returns: The number of rows selected in the given model. + */ +static gint +esma_selected_count (ESelectionModel *selection) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + if (esma->eba) + return e_bit_array_selected_count (esma->eba); + else + return 0; +} + +/** + * e_selection_model_select_all + * @selection: #ESelectionModel to select all + * + * This routine selects all the rows in the given + * #ESelectionModel. + */ +static void +esma_select_all (ESelectionModel *selection) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + + e_selection_model_array_confirm_row_count (esma); + + e_bit_array_select_all (esma->eba); + + esma->cursor_col = 0; + esma->cursor_row_sorted = 0; + esma->cursor_row = es_row_sorted_to_model (esma, esma->cursor_row_sorted); + esma->selection_start_row = esma->cursor_row; + esma->selected_row = -1; + esma->selected_range_end = -1; + e_selection_model_selection_changed (E_SELECTION_MODEL (esma)); + e_selection_model_cursor_changed (E_SELECTION_MODEL (esma), 0, 0); +} + +/** + * e_selection_model_invert_selection + * @selection: #ESelectionModel to invert + * + * This routine inverts all the rows in the given + * #ESelectionModel. + */ +static void +esma_invert_selection (ESelectionModel *selection) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + + e_selection_model_array_confirm_row_count (esma); + + e_bit_array_invert_selection (esma->eba); + + esma->cursor_col = -1; + esma->cursor_row = -1; + esma->cursor_row_sorted = -1; + esma->selection_start_row = 0; + esma->selected_row = -1; + esma->selected_range_end = -1; + e_selection_model_selection_changed (E_SELECTION_MODEL (esma)); + e_selection_model_cursor_changed (E_SELECTION_MODEL (esma), -1, -1); +} + +static gint +esma_row_count (ESelectionModel *selection) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + e_selection_model_array_confirm_row_count (esma); + return e_bit_array_bit_count (esma->eba); +} + +static void +esma_change_one_row (ESelectionModel *selection, + gint row, + gboolean grow) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + e_selection_model_array_confirm_row_count (esma); + e_bit_array_change_one_row (esma->eba, row, grow); +} + +static void +esma_change_cursor (ESelectionModel *selection, + gint row, + gint col) +{ + ESelectionModelArray *esma; + + g_return_if_fail (selection != NULL); + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + esma = E_SELECTION_MODEL_ARRAY (selection); + + esma->cursor_row = row; + esma->cursor_col = col; + esma->cursor_row_sorted = es_row_model_to_sorted (esma, esma->cursor_row); +} + +static void +esma_change_range (ESelectionModel *selection, + gint start, + gint end, + gboolean grow) +{ + gint i; + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + if (start != end) { + if (selection->sorter && e_sorter_needs_sorting (selection->sorter)) { + for (i = start; i < end; i++) { + e_bit_array_change_one_row (esma->eba, e_sorter_sorted_to_model (selection->sorter, i), grow); + } + } else { + e_selection_model_array_confirm_row_count (esma); + e_bit_array_change_range (esma->eba, start, end, grow); + } + } +} + +static gint +esma_cursor_row (ESelectionModel *selection) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + return esma->cursor_row; +} + +static gint +esma_cursor_col (ESelectionModel *selection) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + return esma->cursor_col; +} + +static void +esma_real_select_single_row (ESelectionModel *selection, + gint row) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + + e_selection_model_array_confirm_row_count (esma); + + e_bit_array_select_single_row (esma->eba, row); + + esma->selection_start_row = row; + esma->selected_row = row; + esma->selected_range_end = row; +} + +static void +esma_select_single_row (ESelectionModel *selection, + gint row) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + gint selected_row = esma->selected_row; + esma_real_select_single_row (selection, row); + + if (selected_row != -1 && esma->eba && selected_row < e_bit_array_bit_count (esma->eba)) { + if (selected_row != row) { + e_selection_model_selection_row_changed (selection, selected_row); + e_selection_model_selection_row_changed (selection, row); + } + } else { + e_selection_model_selection_changed (selection); + } +} + +static void +esma_toggle_single_row (ESelectionModel *selection, + gint row) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + + e_selection_model_array_confirm_row_count (esma); + e_bit_array_toggle_single_row (esma->eba, row); + + esma->selection_start_row = row; + esma->selected_row = -1; + esma->selected_range_end = -1; + e_selection_model_selection_row_changed (E_SELECTION_MODEL (esma), row); +} + +static void +esma_real_move_selection_end (ESelectionModel *selection, + gint row) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + gint old_start; + gint old_end; + gint new_start; + gint new_end; + if (selection->sorter && e_sorter_needs_sorting (selection->sorter)) { + old_start = MIN ( + e_sorter_model_to_sorted (selection->sorter, esma->selection_start_row), + e_sorter_model_to_sorted (selection->sorter, esma->cursor_row)); + old_end = MAX ( + e_sorter_model_to_sorted (selection->sorter, esma->selection_start_row), + e_sorter_model_to_sorted (selection->sorter, esma->cursor_row)) + 1; + new_start = MIN ( + e_sorter_model_to_sorted (selection->sorter, esma->selection_start_row), + e_sorter_model_to_sorted (selection->sorter, row)); + new_end = MAX ( + e_sorter_model_to_sorted (selection->sorter, esma->selection_start_row), + e_sorter_model_to_sorted (selection->sorter, row)) + 1; + } else { + old_start = MIN (esma->selection_start_row, esma->cursor_row); + old_end = MAX (esma->selection_start_row, esma->cursor_row) + 1; + new_start = MIN (esma->selection_start_row, row); + new_end = MAX (esma->selection_start_row, row) + 1; + } + /* This wouldn't work nearly so smoothly if one end of the selection weren't held in place. */ + if (old_start < new_start) + esma_change_range (selection, old_start, new_start, FALSE); + if (new_start < old_start) + esma_change_range (selection, new_start, old_start, TRUE); + if (old_end < new_end) + esma_change_range (selection, old_end, new_end, TRUE); + if (new_end < old_end) + esma_change_range (selection, new_end, old_end, FALSE); + esma->selected_row = -1; + esma->selected_range_end = -1; +} + +static void +esma_move_selection_end (ESelectionModel *selection, + gint row) +{ + esma_real_move_selection_end (selection, row); + e_selection_model_selection_changed (selection); +} + +static void +esma_set_selection_end (ESelectionModel *selection, + gint row) +{ + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (selection); + gint selected_range_end = esma->selected_range_end; + gint view_row = e_sorter_model_to_sorted (selection->sorter, row); + + esma_real_select_single_row (selection, esma->selection_start_row); + esma->cursor_row = esma->selection_start_row; + esma->cursor_row_sorted = es_row_model_to_sorted (esma, esma->cursor_row); + esma_real_move_selection_end (selection, row); + + esma->selected_range_end = view_row; + if (selected_range_end != -1 && view_row != -1) { + if (selected_range_end == view_row - 1 || + selected_range_end == view_row + 1) { + e_selection_model_selection_row_changed (selection, selected_range_end); + e_selection_model_selection_row_changed (selection, view_row); + } + } + e_selection_model_selection_changed (selection); +} + +gint +e_selection_model_array_get_row_count (ESelectionModelArray *esma) +{ + g_return_val_if_fail (esma != NULL, 0); + g_return_val_if_fail (E_IS_SELECTION_MODEL_ARRAY (esma), 0); + + if (E_SELECTION_MODEL_ARRAY_GET_CLASS (esma)->get_row_count) + return E_SELECTION_MODEL_ARRAY_GET_CLASS (esma)->get_row_count (esma); + else + return 0; +} + +static void +e_selection_model_array_init (ESelectionModelArray *esma) +{ + esma->eba = NULL; + esma->selection_start_row = 0; + esma->cursor_row = -1; + esma->cursor_col = -1; + esma->cursor_row_sorted = -1; + + esma->selected_row = -1; + esma->selected_range_end = -1; +} + +static void +e_selection_model_array_class_init (ESelectionModelArrayClass *class) +{ + GObjectClass *object_class; + ESelectionModelClass *esm_class; + + object_class = G_OBJECT_CLASS (class); + esm_class = E_SELECTION_MODEL_CLASS (class); + + object_class->dispose = esma_dispose; + object_class->get_property = esma_get_property; + object_class->set_property = esma_set_property; + + esm_class->is_row_selected = esma_is_row_selected; + esm_class->foreach = esma_foreach; + esm_class->clear = esma_clear; + esm_class->selected_count = esma_selected_count; + esm_class->select_all = esma_select_all; + esm_class->invert_selection = esma_invert_selection; + esm_class->row_count = esma_row_count; + + esm_class->change_one_row = esma_change_one_row; + esm_class->change_cursor = esma_change_cursor; + esm_class->cursor_row = esma_cursor_row; + esm_class->cursor_col = esma_cursor_col; + + esm_class->select_single_row = esma_select_single_row; + esm_class->toggle_single_row = esma_toggle_single_row; + esm_class->move_selection_end = esma_move_selection_end; + esm_class->set_selection_end = esma_set_selection_end; + + class->get_row_count = NULL; + + g_object_class_install_property ( + object_class, + PROP_CURSOR_ROW, + g_param_spec_int ( + "cursor_row", + "Cursor Row", + NULL, + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_COL, + g_param_spec_int ( + "cursor_col", + "Cursor Column", + NULL, + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); +} + diff --git a/e-util/e-selection-model-array.h b/e-util/e-selection-model-array.h new file mode 100644 index 0000000000..7292a3365e --- /dev/null +++ b/e-util/e-selection-model-array.h @@ -0,0 +1,95 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_SELECTION_MODEL_ARRAY_H_ +#define _E_SELECTION_MODEL_ARRAY_H_ + +#include <e-util/e-bit-array.h> +#include <e-util/e-selection-model.h> + +G_BEGIN_DECLS + +#define E_SELECTION_MODEL_ARRAY_TYPE (e_selection_model_array_get_type ()) +#define E_SELECTION_MODEL_ARRAY(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), E_SELECTION_MODEL_ARRAY_TYPE, ESelectionModelArray)) +#define E_SELECTION_MODEL_ARRAY_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), E_SELECTION_MODEL_ARRAY_TYPE, ESelectionModelArrayClass)) +#define E_IS_SELECTION_MODEL_ARRAY(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), E_SELECTION_MODEL_ARRAY_TYPE)) +#define E_IS_SELECTION_MODEL_ARRAY_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), E_SELECTION_MODEL_ARRAY_TYPE)) +#define E_SELECTION_MODEL_ARRAY_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), E_SELECTION_MODEL_ARRAY_TYPE, ESelectionModelArrayClass)) + +typedef struct { + ESelectionModel base; + + EBitArray *eba; + + gint cursor_row; + gint cursor_col; + gint selection_start_row; + gint cursor_row_sorted; /* cursor_row passed through base::sorter if necessary */ + + guint model_changed_id; + guint model_row_inserted_id, model_row_deleted_id; + + /* Anything other than -1 means that the selection is a single + * row. This being -1 does not impart any information. */ + gint selected_row; + /* Anything other than -1 means that the selection is a all + * rows between selection_start_path and cursor_path where + * selected_range_end is the rwo number of cursor_path. This + * being -1 does not impart any information. */ + gint selected_range_end; + + guint frozen : 1; + guint selection_model_changed : 1; + guint group_info_changed : 1; +} ESelectionModelArray; + +typedef struct { + ESelectionModelClass parent_class; + + gint (*get_row_count) (ESelectionModelArray *selection); +} ESelectionModelArrayClass; + +GType e_selection_model_array_get_type (void); + +/* Protected Functions */ +void e_selection_model_array_insert_rows (ESelectionModelArray *esm, + gint row, + gint count); +void e_selection_model_array_delete_rows (ESelectionModelArray *esm, + gint row, + gint count); +void e_selection_model_array_move_row (ESelectionModelArray *esm, + gint old_row, + gint new_row); +void e_selection_model_array_confirm_row_count (ESelectionModelArray *esm); + +/* Protected Virtual Function */ +gint e_selection_model_array_get_row_count (ESelectionModelArray *esm); + +G_END_DECLS + +#endif /* _E_SELECTION_MODEL_ARRAY_H_ */ diff --git a/e-util/e-selection-model-simple.c b/e-util/e-selection-model-simple.c new file mode 100644 index 0000000000..f7123dd09e --- /dev/null +++ b/e-util/e-selection-model-simple.c @@ -0,0 +1,117 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-selection-model-array.h" +#include "e-selection-model-simple.h" + +static gint esms_get_row_count (ESelectionModelArray *esma); + +G_DEFINE_TYPE ( + ESelectionModelSimple, + e_selection_model_simple, + E_SELECTION_MODEL_ARRAY_TYPE) + +static void +e_selection_model_simple_init (ESelectionModelSimple *selection) +{ + selection->row_count = 0; +} + +static void +e_selection_model_simple_class_init (ESelectionModelSimpleClass *class) +{ + ESelectionModelArrayClass *esma_class; + + esma_class = E_SELECTION_MODEL_ARRAY_CLASS (class); + esma_class->get_row_count = esms_get_row_count; +} + +/** + * e_selection_model_simple_new + * + * This routine creates a new #ESelectionModelSimple. + * + * Returns: The new #ESelectionModelSimple. + */ +ESelectionModelSimple * +e_selection_model_simple_new (void) +{ + return g_object_new (E_SELECTION_MODEL_SIMPLE_TYPE, NULL); +} + +void +e_selection_model_simple_set_row_count (ESelectionModelSimple *esms, + gint row_count) +{ + if (esms->row_count != row_count) { + ESelectionModelArray *esma = E_SELECTION_MODEL_ARRAY (esms); + if (esma->eba) + g_object_unref (esma->eba); + esma->eba = NULL; + esma->selected_row = -1; + esma->selected_range_end = -1; + } + + esms->row_count = row_count; +} + +static gint +esms_get_row_count (ESelectionModelArray *esma) +{ + ESelectionModelSimple *esms = E_SELECTION_MODEL_SIMPLE (esma); + + return esms->row_count; +} + +void +e_selection_model_simple_insert_rows (ESelectionModelSimple *esms, + gint row, + gint count) +{ + esms->row_count += count; + e_selection_model_array_insert_rows ( + E_SELECTION_MODEL_ARRAY (esms), row, count); +} + +void +e_selection_model_simple_delete_rows (ESelectionModelSimple *esms, + gint row, + gint count) +{ + esms->row_count -= count; + e_selection_model_array_delete_rows ( + E_SELECTION_MODEL_ARRAY (esms), row, count); +} + +void +e_selection_model_simple_move_row (ESelectionModelSimple *esms, + gint old_row, + gint new_row) +{ + e_selection_model_array_move_row ( + E_SELECTION_MODEL_ARRAY (esms), old_row, new_row); +} diff --git a/e-util/e-selection-model-simple.h b/e-util/e-selection-model-simple.h new file mode 100644 index 0000000000..b4551dd51f --- /dev/null +++ b/e-util/e-selection-model-simple.h @@ -0,0 +1,70 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_SELECTION_MODEL_SIMPLE_H_ +#define _E_SELECTION_MODEL_SIMPLE_H_ + +#include <e-util/e-selection-model-array.h> + +G_BEGIN_DECLS + +#define E_SELECTION_MODEL_SIMPLE_TYPE (e_selection_model_simple_get_type ()) +#define E_SELECTION_MODEL_SIMPLE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), E_SELECTION_MODEL_SIMPLE_TYPE, ESelectionModelSimple)) +#define E_SELECTION_MODEL_SIMPLE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), E_SELECTION_MODEL_SIMPLE_TYPE, ESelectionModelSimpleClass)) +#define E_IS_SELECTION_MODEL_SIMPLE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), E_SELECTION_MODEL_SIMPLE_TYPE)) +#define E_IS_SELECTION_MODEL_SIMPLE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), E_SELECTION_MODEL_SIMPLE_TYPE)) + +typedef struct { + ESelectionModelArray parent; + + gint row_count; +} ESelectionModelSimple; + +typedef struct { + ESelectionModelArrayClass parent_class; +} ESelectionModelSimpleClass; + +GType e_selection_model_simple_get_type (void); +ESelectionModelSimple *e_selection_model_simple_new (void); + +void e_selection_model_simple_insert_rows (ESelectionModelSimple *esms, + gint row, + gint count); +void e_selection_model_simple_delete_rows (ESelectionModelSimple *esms, + gint row, + gint count); +void e_selection_model_simple_move_row (ESelectionModelSimple *esms, + gint old_row, + gint new_row); + +void e_selection_model_simple_set_row_count (ESelectionModelSimple *selection, + gint row_count); + +G_END_DECLS + +#endif /* _E_SELECTION_MODEL_SIMPLE_H_ */ + diff --git a/e-util/e-selection-model.c b/e-util/e-selection-model.c new file mode 100644 index 0000000000..4c553f485a --- /dev/null +++ b/e-util/e-selection-model.c @@ -0,0 +1,813 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-selection-model.h" + +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include "e-marshal.h" + +G_DEFINE_TYPE ( + ESelectionModel, + e_selection_model, + G_TYPE_OBJECT) + +enum { + CURSOR_CHANGED, + CURSOR_ACTIVATED, + SELECTION_CHANGED, + SELECTION_ROW_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +enum { + PROP_0, + PROP_SORTER, + PROP_SELECTION_MODE, + PROP_CURSOR_MODE +}; + +inline static void +add_sorter (ESelectionModel *esm, + ESorter *sorter) +{ + esm->sorter = sorter; + if (sorter) { + g_object_ref (sorter); + } +} + +inline static void +drop_sorter (ESelectionModel *esm) +{ + if (esm->sorter) { + g_object_unref (esm->sorter); + } + esm->sorter = NULL; +} + +static void +esm_dispose (GObject *object) +{ + ESelectionModel *esm; + + esm = E_SELECTION_MODEL (object); + + drop_sorter (esm); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_selection_model_parent_class)->dispose (object); +} + +static void +esm_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ESelectionModel *esm = E_SELECTION_MODEL (object); + + switch (property_id) { + case PROP_SORTER: + g_value_set_object (value, esm->sorter); + break; + + case PROP_SELECTION_MODE: + g_value_set_int (value, esm->mode); + break; + + case PROP_CURSOR_MODE: + g_value_set_int (value, esm->cursor_mode); + break; + } +} + +static void +esm_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ESelectionModel *esm = E_SELECTION_MODEL (object); + + switch (property_id) { + case PROP_SORTER: + drop_sorter (esm); + add_sorter ( + esm, g_value_get_object (value) ? + E_SORTER (g_value_get_object (value)) : NULL); + break; + + case PROP_SELECTION_MODE: + esm->mode = g_value_get_int (value); + if (esm->mode == GTK_SELECTION_SINGLE) { + gint cursor_row = e_selection_model_cursor_row (esm); + gint cursor_col = e_selection_model_cursor_col (esm); + e_selection_model_do_something (esm, cursor_row, cursor_col, 0); + } + break; + + case PROP_CURSOR_MODE: + esm->cursor_mode = g_value_get_int (value); + break; + } +} + +static void +e_selection_model_init (ESelectionModel *selection) +{ + selection->mode = GTK_SELECTION_MULTIPLE; + selection->cursor_mode = E_CURSOR_SIMPLE; + selection->old_selection = -1; +} + +static void +e_selection_model_class_init (ESelectionModelClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = esm_dispose; + object_class->get_property = esm_get_property; + object_class->set_property = esm_set_property; + + signals[CURSOR_CHANGED] = g_signal_new ( + "cursor_changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESelectionModelClass, cursor_changed), + NULL, NULL, + e_marshal_NONE__INT_INT, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_INT); + + signals[CURSOR_ACTIVATED] = g_signal_new ( + "cursor_activated", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESelectionModelClass, cursor_activated), + NULL, NULL, + e_marshal_NONE__INT_INT, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_INT); + + signals[SELECTION_CHANGED] = g_signal_new ( + "selection_changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESelectionModelClass, selection_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[SELECTION_ROW_CHANGED] = g_signal_new ( + "selection_row_changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESelectionModelClass, selection_row_changed), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + g_object_class_install_property ( + object_class, + PROP_SORTER, + g_param_spec_object ( + "sorter", + "Sorter", + NULL, + E_SORTER_TYPE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SELECTION_MODE, + g_param_spec_int ( + "selection_mode", + "Selection Mode", + NULL, + GTK_SELECTION_NONE, + GTK_SELECTION_MULTIPLE, + GTK_SELECTION_SINGLE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_MODE, + g_param_spec_int ( + "cursor_mode", + "Cursor Mode", + NULL, + E_CURSOR_LINE, + E_CURSOR_SPREADSHEET, + E_CURSOR_LINE, + G_PARAM_READWRITE)); +} + +/** + * e_selection_model_is_row_selected + * @selection: #ESelectionModel to check + * @n: The row to check + * + * This routine calculates whether the given row is selected. + * + * Returns: %TRUE if the given row is selected + */ +gboolean +e_selection_model_is_row_selected (ESelectionModel *selection, + gint n) +{ + ESelectionModelClass *class; + + g_return_val_if_fail (E_IS_SELECTION_MODEL (selection), FALSE); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_val_if_fail (class->is_row_selected != NULL, FALSE); + + return class->is_row_selected (selection, n); +} + +/** + * e_selection_model_foreach + * @selection: #ESelectionModel to traverse + * @callback: The callback function to call back. + * @closure: The closure + * + * This routine calls the given callback function once for each + * selected row, passing closure as the closure. + */ +void +e_selection_model_foreach (ESelectionModel *selection, + EForeachFunc callback, + gpointer closure) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + g_return_if_fail (callback != NULL); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->foreach != NULL); + + class->foreach (selection, callback, closure); +} + +/** + * e_selection_model_clear + * @selection: #ESelectionModel to clear + * + * This routine clears the selection to no rows selected. + */ +void +e_selection_model_clear (ESelectionModel *selection) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->clear != NULL); + + class->clear (selection); +} + +/** + * e_selection_model_selected_count + * @selection: #ESelectionModel to count + * + * This routine calculates the number of rows selected. + * + * Returns: The number of rows selected in the given model. + */ +gint +e_selection_model_selected_count (ESelectionModel *selection) +{ + ESelectionModelClass *class; + + g_return_val_if_fail (E_IS_SELECTION_MODEL (selection), 0); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_val_if_fail (class->selected_count != NULL, 0); + + return class->selected_count (selection); +} + +/** + * e_selection_model_select_all + * @selection: #ESelectionModel to select all + * + * This routine selects all the rows in the given + * #ESelectionModel. + */ +void +e_selection_model_select_all (ESelectionModel *selection) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->select_all != NULL); + + class->select_all (selection); +} + +/** + * e_selection_model_invert_selection + * @selection: #ESelectionModel to invert + * + * This routine inverts all the rows in the given + * #ESelectionModel. + */ +void +e_selection_model_invert_selection (ESelectionModel *selection) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->invert_selection != NULL); + + class->invert_selection (selection); +} + +gint +e_selection_model_row_count (ESelectionModel *selection) +{ + ESelectionModelClass *class; + + g_return_val_if_fail (E_IS_SELECTION_MODEL (selection), 0); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_val_if_fail (class->row_count != NULL, 0); + + return class->row_count (selection); +} + +void +e_selection_model_change_one_row (ESelectionModel *selection, + gint row, + gboolean grow) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->change_one_row != NULL); + + return class->change_one_row (selection, row, grow); +} + +void +e_selection_model_change_cursor (ESelectionModel *selection, + gint row, + gint col) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->change_cursor != NULL); + + class->change_cursor (selection, row, col); +} + +gint +e_selection_model_cursor_row (ESelectionModel *selection) +{ + ESelectionModelClass *class; + + g_return_val_if_fail (E_IS_SELECTION_MODEL (selection), -1); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_val_if_fail (class->cursor_row != NULL, -1); + + return class->cursor_row (selection); +} + +gint +e_selection_model_cursor_col (ESelectionModel *selection) +{ + ESelectionModelClass *class; + + g_return_val_if_fail (E_IS_SELECTION_MODEL (selection), -1); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_val_if_fail (class->cursor_col != NULL, -1); + + return class->cursor_col (selection); +} + +void +e_selection_model_select_single_row (ESelectionModel *selection, + gint row) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->select_single_row != NULL); + + class->select_single_row (selection, row); +} + +void +e_selection_model_toggle_single_row (ESelectionModel *selection, + gint row) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->toggle_single_row != NULL); + + class->toggle_single_row (selection, row); +} + +void +e_selection_model_move_selection_end (ESelectionModel *selection, + gint row) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->move_selection_end != NULL); + + class->move_selection_end (selection, row); +} + +void +e_selection_model_set_selection_end (ESelectionModel *selection, + gint row) +{ + ESelectionModelClass *class; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + class = E_SELECTION_MODEL_GET_CLASS (selection); + g_return_if_fail (class->set_selection_end != NULL); + + class->set_selection_end (selection, row); +} + +/** + * e_selection_model_do_something + * @selection: #ESelectionModel to do something to. + * @row: The row to do something in. + * @col: The col to do something in. + * @state: The state in which to do something. + * + * This routine does whatever is appropriate as if the user clicked + * the mouse in the given row and column. + */ +void +e_selection_model_do_something (ESelectionModel *selection, + guint row, + guint col, + GdkModifierType state) +{ + gint shift_p = state & GDK_SHIFT_MASK; + gint ctrl_p = state & GDK_CONTROL_MASK; + gint row_count; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + selection->old_selection = -1; + + if (row == -1 && col != -1) + row = 0; + if (col == -1 && row != -1) + col = 0; + + row_count = e_selection_model_row_count (selection); + if (row_count >= 0 && row < row_count) { + switch (selection->mode) { + case GTK_SELECTION_SINGLE: + e_selection_model_select_single_row (selection, row); + break; + case GTK_SELECTION_BROWSE: + case GTK_SELECTION_MULTIPLE: + if (shift_p) { + e_selection_model_set_selection_end (selection, row); + } else { + if (ctrl_p) { + e_selection_model_toggle_single_row (selection, row); + } else { + e_selection_model_select_single_row (selection, row); + } + } + break; + default: + g_return_if_reached (); + break; + } + e_selection_model_change_cursor (selection, row, col); + g_signal_emit ( + selection, + signals[CURSOR_CHANGED], 0, + row, col); + g_signal_emit ( + selection, + signals[CURSOR_ACTIVATED], 0, + row, col); + } +} + +/** + * e_selection_model_maybe_do_something + * @selection: #ESelectionModel to do something to. + * @row: The row to do something in. + * @col: The col to do something in. + * @state: The state in which to do something. + * + * If this row is selected, this routine just moves the cursor row and + * column. Otherwise, it does the same thing as + * e_selection_model_do_something(). This is for being used on + * right clicks and other events where if the user hit the selection, + * they don't want it to change. + */ +gboolean +e_selection_model_maybe_do_something (ESelectionModel *selection, + guint row, + guint col, + GdkModifierType state) +{ + g_return_val_if_fail (E_IS_SELECTION_MODEL (selection), FALSE); + + selection->old_selection = -1; + + if (e_selection_model_is_row_selected (selection, row)) { + e_selection_model_change_cursor (selection, row, col); + g_signal_emit ( + selection, + signals[CURSOR_CHANGED], 0, + row, col); + return FALSE; + } else { + e_selection_model_do_something (selection, row, col, state); + return TRUE; + } +} + +void +e_selection_model_right_click_down (ESelectionModel *selection, + guint row, + guint col, + GdkModifierType state) +{ + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + if (selection->mode == GTK_SELECTION_SINGLE) { + selection->old_selection = + e_selection_model_cursor_row (selection); + e_selection_model_select_single_row (selection, row); + } else { + e_selection_model_maybe_do_something ( + selection, row, col, state); + } +} + +void +e_selection_model_right_click_up (ESelectionModel *selection) +{ + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + if (selection->mode != GTK_SELECTION_SINGLE) + return; + + if (selection->old_selection == -1) + return; + + e_selection_model_select_single_row ( + selection, selection->old_selection); +} + +void +e_selection_model_select_as_key_press (ESelectionModel *selection, + guint row, + guint col, + GdkModifierType state) +{ + gint cursor_activated = TRUE; + + gint shift_p = state & GDK_SHIFT_MASK; + gint ctrl_p = state & GDK_CONTROL_MASK; + + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + selection->old_selection = -1; + + switch (selection->mode) { + case GTK_SELECTION_BROWSE: + case GTK_SELECTION_MULTIPLE: + if (shift_p) { + e_selection_model_set_selection_end (selection, row); + } else if (!ctrl_p) { + e_selection_model_select_single_row (selection, row); + } else + cursor_activated = FALSE; + break; + case GTK_SELECTION_SINGLE: + e_selection_model_select_single_row (selection, row); + break; + default: + g_return_if_reached (); + break; + } + if (row != -1) { + e_selection_model_change_cursor (selection, row, col); + g_signal_emit ( + selection, + signals[CURSOR_CHANGED], 0, + row, col); + if (cursor_activated) + g_signal_emit ( + selection, + signals[CURSOR_ACTIVATED], 0, + row, col); + } +} + +static gint +move_selection (ESelectionModel *selection, + gboolean up, + GdkModifierType state) +{ + gint row = e_selection_model_cursor_row (selection); + gint col = e_selection_model_cursor_col (selection); + gint row_count; + + /* there is no selected row when row is -1 */ + if (row != -1) + row = e_sorter_model_to_sorted (selection->sorter, row); + + if (up) + row--; + else + row++; + if (row < 0) + row = 0; + row_count = e_selection_model_row_count (selection); + if (row >= row_count) + row = row_count - 1; + row = e_sorter_sorted_to_model (selection->sorter, row); + + e_selection_model_select_as_key_press (selection, row, col, state); + return TRUE; +} + +/** + * e_selection_model_key_press + * @selection: #ESelectionModel to affect. + * @key: The event. + * + * This routine does whatever is appropriate as if the user pressed + * the given key. + * + * Returns: %TRUE if the #ESelectionModel used the key. + */ +gboolean +e_selection_model_key_press (ESelectionModel *selection, + GdkEventKey *key) +{ + g_return_val_if_fail (E_IS_SELECTION_MODEL (selection), FALSE); + g_return_val_if_fail (key != NULL, FALSE); + + selection->old_selection = -1; + + switch (key->keyval) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + return move_selection (selection, TRUE, key->state); + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + return move_selection (selection, FALSE, key->state); + case GDK_KEY_space: + case GDK_KEY_KP_Space: + if (selection->mode != GTK_SELECTION_SINGLE) { + gint row = e_selection_model_cursor_row (selection); + gint col = e_selection_model_cursor_col (selection); + if (row == -1) + break; + + e_selection_model_toggle_single_row (selection, row); + g_signal_emit ( + selection, + signals[CURSOR_ACTIVATED], 0, + row, col); + return TRUE; + } + break; + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (selection->mode != GTK_SELECTION_SINGLE) { + gint row = e_selection_model_cursor_row (selection); + gint col = e_selection_model_cursor_col (selection); + e_selection_model_select_single_row (selection, row); + g_signal_emit ( + selection, + signals[CURSOR_ACTIVATED], 0, + row, col); + return TRUE; + } + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + if (selection->cursor_mode == E_CURSOR_LINE) { + gint row = 0; + gint cursor_col = e_selection_model_cursor_col (selection); + + row = e_sorter_sorted_to_model (selection->sorter, row); + e_selection_model_select_as_key_press ( + selection, row, cursor_col, key->state); + return TRUE; + } + break; + case GDK_KEY_End: + case GDK_KEY_KP_End: + if (selection->cursor_mode == E_CURSOR_LINE) { + gint row = e_selection_model_row_count (selection) - 1; + gint cursor_col = e_selection_model_cursor_col (selection); + + row = e_sorter_sorted_to_model (selection->sorter, row); + e_selection_model_select_as_key_press ( + selection, row, cursor_col, key->state); + return TRUE; + } + break; + } + return FALSE; +} + +void +e_selection_model_cursor_changed (ESelectionModel *selection, + gint row, + gint col) +{ + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + g_signal_emit (selection, signals[CURSOR_CHANGED], 0, row, col); +} + +void +e_selection_model_cursor_activated (ESelectionModel *selection, + gint row, + gint col) +{ + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + g_signal_emit (selection, signals[CURSOR_ACTIVATED], 0, row, col); +} + +void +e_selection_model_selection_changed (ESelectionModel *selection) +{ + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + g_signal_emit (selection, signals[SELECTION_CHANGED], 0); +} + +void +e_selection_model_selection_row_changed (ESelectionModel *selection, + gint row) +{ + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + g_signal_emit (selection, signals[SELECTION_ROW_CHANGED], 0, row); +} diff --git a/e-util/e-selection-model.h b/e-util/e-selection-model.h new file mode 100644 index 0000000000..1d59e28fe1 --- /dev/null +++ b/e-util/e-selection-model.h @@ -0,0 +1,209 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SELECTION_MODEL_H +#define E_SELECTION_MODEL_H + +#include <gtk/gtk.h> +#include <e-util/e-sorter.h> + +/* Standard GObject macros */ +#define E_TYPE_SELECTION_MODEL \ + (e_selection_model_get_type ()) +#define E_SELECTION_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SELECTION_MODEL, ESelectionModel)) +#define E_SELECTION_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SELECTION_MODEL, ESelectionModelClass)) +#define E_IS_SELECTION_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SELECTION_MODEL)) +#define E_IS_SELECTION_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SELECTION_MODEL)) +#define E_SELECTION_MODEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SELECTION_MODEL, ESelectionModelClass)) + +G_BEGIN_DECLS + +#ifndef _E_FOREACH_FUNC_H_ +#define _E_FOREACH_FUNC_H_ +typedef void (*EForeachFunc) (gint model_row, + gpointer closure); +#endif + +typedef struct _ESelectionModel ESelectionModel; +typedef struct _ESelectionModelClass ESelectionModelClass; + +/* list selection modes */ +typedef enum { + E_CURSOR_LINE, + E_CURSOR_SIMPLE, + E_CURSOR_SPREADSHEET +} ECursorMode; + +struct _ESelectionModel { + GObject parent; + + ESorter *sorter; + + GtkSelectionMode mode; + ECursorMode cursor_mode; + + gint old_selection; +}; + +struct _ESelectionModelClass { + GObjectClass parent_class; + + /* Virtual methods */ + gboolean (*is_row_selected) (ESelectionModel *esm, + gint row); + void (*foreach) (ESelectionModel *esm, + EForeachFunc callback, + gpointer closure); + void (*clear) (ESelectionModel *esm); + gint (*selected_count) (ESelectionModel *esm); + void (*select_all) (ESelectionModel *esm); + void (*invert_selection) (ESelectionModel *esm); + gint (*row_count) (ESelectionModel *esm); + + /* Protected virtual methods. */ + void (*change_one_row) (ESelectionModel *esm, + gint row, + gboolean on); + void (*change_cursor) (ESelectionModel *esm, + gint row, + gint col); + gint (*cursor_row) (ESelectionModel *esm); + gint (*cursor_col) (ESelectionModel *esm); + + void (*select_single_row) (ESelectionModel *selection, + gint row); + void (*toggle_single_row) (ESelectionModel *selection, + gint row); + void (*move_selection_end) (ESelectionModel *selection, + gint row); + void (*set_selection_end) (ESelectionModel *selection, + gint row); + + /* Signals */ + void (*cursor_changed) (ESelectionModel *esm, + gint row, + gint col); + void (*cursor_activated) (ESelectionModel *esm, + gint row, + gint col); + void (*selection_row_changed)(ESelectionModel *esm, + gint row); + void (*selection_changed) (ESelectionModel *esm); +}; + +GType e_selection_model_get_type (void); +void e_selection_model_do_something (ESelectionModel *esm, + guint row, + guint col, + GdkModifierType state); +gboolean e_selection_model_maybe_do_something + (ESelectionModel *esm, + guint row, + guint col, + GdkModifierType state); +void e_selection_model_right_click_down + (ESelectionModel *selection, + guint row, + guint col, + GdkModifierType state); +void e_selection_model_right_click_up + (ESelectionModel *selection); +gboolean e_selection_model_key_press (ESelectionModel *esm, + GdkEventKey *key); +void e_selection_model_select_as_key_press + (ESelectionModel *esm, + guint row, + guint col, + GdkModifierType state); + +/* Virtual functions */ +gboolean e_selection_model_is_row_selected + (ESelectionModel *esm, + gint n); +void e_selection_model_foreach (ESelectionModel *esm, + EForeachFunc callback, + gpointer closure); +void e_selection_model_clear (ESelectionModel *esm); +gint e_selection_model_selected_count + (ESelectionModel *esm); +void e_selection_model_select_all (ESelectionModel *esm); +void e_selection_model_invert_selection + (ESelectionModel *esm); +gint e_selection_model_row_count (ESelectionModel *esm); + +/* Private virtual Functions */ +void e_selection_model_change_one_row + (ESelectionModel *esm, + gint row, + gboolean on); +void e_selection_model_change_cursor (ESelectionModel *esm, + gint row, + gint col); +gint e_selection_model_cursor_row (ESelectionModel *esm); +gint e_selection_model_cursor_col (ESelectionModel *esm); +void e_selection_model_select_single_row + (ESelectionModel *selection, + gint row); +void e_selection_model_toggle_single_row + (ESelectionModel *selection, + gint row); +void e_selection_model_move_selection_end + (ESelectionModel *selection, + gint row); +void e_selection_model_set_selection_end + (ESelectionModel *selection, + gint row); + +/* Signals */ +void e_selection_model_cursor_changed + (ESelectionModel *selection, + gint row, + gint col); +void e_selection_model_cursor_activated + (ESelectionModel *selection, + gint row, + gint col); +void e_selection_model_selection_row_changed + (ESelectionModel *selection, + gint row); +void e_selection_model_selection_changed + (ESelectionModel *selection); + +G_END_DECLS + +#endif /* E_SELECTION_MODEL_H */ + diff --git a/e-util/e-selection.c b/e-util/e-selection.c index 041b30c4f2..4092b07cbe 100644 --- a/e-util/e-selection.c +++ b/e-util/e-selection.c @@ -22,7 +22,7 @@ /** * SECTION: e-selection * @short_description: selection and clipboard utilities - * @include: e-util/e-selection.h + * @include: e-util/e-util.h **/ #ifdef HAVE_CONFIG_H diff --git a/e-util/e-selection.h b/e-util/e-selection.h index 5d44cd4839..75089c4bc1 100644 --- a/e-util/e-selection.h +++ b/e-util/e-selection.h @@ -19,6 +19,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_SELECTION_H #define E_SELECTION_H diff --git a/e-util/e-send-options.c b/e-util/e-send-options.c new file mode 100644 index 0000000000..bf50dbefc0 --- /dev/null +++ b/e-util/e-send-options.c @@ -0,0 +1,767 @@ +/* + * Evolution calendar - Main page of the Groupwise send options Dialog + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chenthill Palanisamy <pchenthill@novell.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-send-options.h" + +#include <string.h> +#include <glib/gi18n.h> +#include <time.h> + +#include "e-dateedit.h" +#include "e-misc-utils.h" +#include "e-util-private.h" + +#define E_SEND_OPTIONS_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SEND_OPTIONS_DIALOG, ESendOptionsDialogPrivate)) + +struct _ESendOptionsDialogPrivate { + GtkBuilder *builder; + + gboolean gopts_needed; + gboolean global; + + /* Widgets */ + + GtkWidget *main; + + /* Noteboook to add options page and status tracking page*/ + GtkNotebook *notebook; + + GtkWidget *status; + + /* priority */ + GtkWidget *priority; + + /* Security */ + GtkWidget *security; + + /* Widgets for Reply Requestion options */ + GtkWidget *reply_request; + GtkWidget *reply_convenient; + GtkWidget *reply_within; + GtkWidget *within_days; + + /* Widgets for delay delivery Option */ + GtkWidget *delay_delivery; + GtkWidget *delay_until; + + /* Widgets for Choosing expiration date */ + GtkWidget *expiration; + GtkWidget *expire_after; + + /* Widgets to for tracking information through sent Item */ + GtkWidget *create_sent; + GtkWidget *delivered; + GtkWidget *delivered_opened; + GtkWidget *all_info; + GtkWidget *autodelete; + + /* Widgets for setting the Return Notification */ + GtkWidget *when_opened; + GtkWidget *when_declined; + GtkWidget *when_accepted; + GtkWidget *when_completed; + + /* label widgets */ + GtkWidget *security_label; + GtkWidget *priority_label; + GtkWidget *gopts_label; + GtkWidget *opened_label; + GtkWidget *declined_label; + GtkWidget *accepted_label; + GtkWidget *completed_label; + GtkWidget *until_label; + gchar *help_section; +}; + +static void e_send_options_dialog_finalize (GObject *object); +static void e_send_options_cb (GtkDialog *dialog, gint state, gpointer func_data); + +enum { + SOD_RESPONSE, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_TYPE ( + ESendOptionsDialog, + e_send_options_dialog, + G_TYPE_OBJECT) + +static void +e_send_options_get_widgets_data (ESendOptionsDialog *sod) +{ + ESendOptionsDialogPrivate *priv; + ESendOptionsGeneral *gopts; + ESendOptionsStatusTracking *sopts; + + priv = sod->priv; + gopts = sod->data->gopts; + sopts = sod->data->sopts; + + gopts->priority = gtk_combo_box_get_active ((GtkComboBox *) priv->priority); + gopts->security = gtk_combo_box_get_active ((GtkComboBox *) priv->security); + + gopts->reply_enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->reply_request)); + gopts->reply_convenient = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->reply_convenient)); + gopts->reply_within = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (priv->within_days)); + + gopts->expiration_enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->expiration)); + gopts->expire_after = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (priv->expire_after)); + gopts->delay_enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->delay_delivery)); + + if (e_date_edit_date_is_valid (E_DATE_EDIT (priv->delay_until)) && + e_date_edit_time_is_valid (E_DATE_EDIT (priv->delay_until))) + gopts->delay_until = e_date_edit_get_time (E_DATE_EDIT (priv->delay_until)); + + sopts->tracking_enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->create_sent)); + + sopts->autodelete = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->autodelete)); + + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->delivered))) + sopts->track_when = E_DELIVERED; + else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->delivered_opened))) + sopts->track_when = E_DELIVERED_OPENED; + else + sopts->track_when = E_ALL; + + sopts->opened = gtk_combo_box_get_active ((GtkComboBox *) priv->when_opened); + sopts->accepted = gtk_combo_box_get_active ((GtkComboBox *) priv->when_accepted); + sopts->declined = gtk_combo_box_get_active ((GtkComboBox *) priv->when_declined); + sopts->completed = gtk_combo_box_get_active ((GtkComboBox *) priv->when_completed); +} + +static void +e_send_options_fill_widgets_with_data (ESendOptionsDialog *sod) +{ + ESendOptionsDialogPrivate *priv; + ESendOptionsGeneral *gopts; + ESendOptionsStatusTracking *sopts; + time_t tmp; + + priv = sod->priv; + gopts = sod->data->gopts; + sopts = sod->data->sopts; + tmp = time (NULL); + + gtk_combo_box_set_active ((GtkComboBox *) priv->priority, gopts->priority); + gtk_combo_box_set_active ((GtkComboBox *) priv->security, gopts->security); + + if (gopts->reply_enabled) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->reply_request), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->reply_request), FALSE); + + if (gopts->reply_convenient) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->reply_convenient), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->reply_within), TRUE); + + gtk_spin_button_set_value (GTK_SPIN_BUTTON (priv->within_days), (gdouble) gopts->reply_within); + + if (gopts->expiration_enabled) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->expiration), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->expiration), FALSE); + + gtk_spin_button_set_value (GTK_SPIN_BUTTON (priv->expire_after), (gdouble) gopts->expire_after); + + if (gopts->delay_enabled) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->delay_delivery), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->delay_delivery), FALSE); + + if (!gopts->delay_until || difftime (gopts->delay_until, tmp) < 0) + e_date_edit_set_time (E_DATE_EDIT (priv->delay_until), 0); + else + e_date_edit_set_time (E_DATE_EDIT (priv->delay_until), gopts->delay_until); + + if (sopts->tracking_enabled) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->create_sent), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->create_sent), FALSE); + + if (sopts->autodelete) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->autodelete), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->autodelete), FALSE); + + switch (sopts->track_when) { + case E_DELIVERED: + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->delivered), TRUE); + break; + case E_DELIVERED_OPENED: + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->delivered_opened), TRUE); + break; + case E_ALL: + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->all_info), TRUE); + } + + gtk_combo_box_set_active ((GtkComboBox *) priv->when_opened, sopts->opened); + gtk_combo_box_set_active ((GtkComboBox *) priv->when_declined, sopts->declined); + gtk_combo_box_set_active ((GtkComboBox *) priv->when_accepted, sopts->accepted); + gtk_combo_box_set_active ((GtkComboBox *) priv->when_completed, sopts->completed); +} + +static void +sensitize_widgets (ESendOptionsDialog *sod) +{ + ESendOptionsDialogPrivate *priv; + ESendOptionsGeneral *gopts; + ESendOptionsStatusTracking *sopts; + + priv = sod->priv; + gopts = sod->data->gopts; + sopts = sod->data->sopts; + + if (!gopts->reply_enabled) { + gtk_widget_set_sensitive (priv->reply_convenient, FALSE); + gtk_widget_set_sensitive (priv->reply_within, FALSE); + gtk_widget_set_sensitive (priv->within_days, FALSE); + } + + if (!gopts->expiration_enabled) + gtk_widget_set_sensitive (priv->expire_after, FALSE); + + if (!gopts->delay_enabled) { + gtk_widget_set_sensitive (priv->delay_until, FALSE); + } + + if (!sopts->tracking_enabled) { + gtk_widget_set_sensitive (priv->delivered, FALSE); + gtk_widget_set_sensitive (priv->delivered_opened, FALSE); + gtk_widget_set_sensitive (priv->all_info, FALSE); + gtk_widget_set_sensitive (priv->autodelete, FALSE); + } +} + +static void +expiration_toggled_cb (GtkToggleButton *toggle, + gpointer data) +{ + gboolean active; + ESendOptionsDialog *sod; + ESendOptionsDialogPrivate *priv; + + sod = data; + priv = sod->priv; + + active = gtk_toggle_button_get_active (toggle); + + gtk_widget_set_sensitive (priv->expire_after, active); +} + +static void +reply_request_toggled_cb (GtkToggleButton *toggle, + gpointer data) +{ + gboolean active; + ESendOptionsDialog *sod; + ESendOptionsDialogPrivate *priv; + + sod = data; + priv = sod->priv; + active = gtk_toggle_button_get_active (toggle); + + gtk_widget_set_sensitive (priv->reply_convenient, active); + gtk_widget_set_sensitive (priv->reply_within, active); + gtk_widget_set_sensitive (priv->within_days, active); + +} + +static void +delay_delivery_toggled_cb (GtkToggleButton *toggle, + gpointer data) +{ + gboolean active; + ESendOptionsDialog *sod; + ESendOptionsDialogPrivate *priv; + + sod = data; + priv = sod->priv; + active = gtk_toggle_button_get_active (toggle); + + gtk_widget_set_sensitive (priv->delay_until, active); +} + +static void +sent_item_toggled_cb (GtkToggleButton *toggle, + gpointer data) +{ + gboolean active; + ESendOptionsDialog *sod; + ESendOptionsDialogPrivate *priv; + + sod = data; + priv = sod->priv; + active = gtk_toggle_button_get_active (toggle); + + gtk_widget_set_sensitive (priv->delivered, active); + gtk_widget_set_sensitive (priv->delivered_opened, active); + gtk_widget_set_sensitive (priv->all_info, active); + gtk_widget_set_sensitive (priv->autodelete, active); +} + +static void +delay_until_date_changed_cb (GtkWidget *dedit, + gpointer data) +{ + ESendOptionsDialog *sod; + ESendOptionsDialogPrivate *priv; + time_t tmp, current; + + sod = data; + priv = sod->priv; + + current = time (NULL); + tmp = e_date_edit_get_time (E_DATE_EDIT (priv->delay_until)); + + if ((difftime (tmp, current) < 0) || !e_date_edit_time_is_valid (E_DATE_EDIT (priv->delay_until)) + || !e_date_edit_date_is_valid (E_DATE_EDIT (priv->delay_until))) + e_date_edit_set_time (E_DATE_EDIT (priv->delay_until), 0); + +} +static void +page_changed_cb (GtkNotebook *notebook, + GtkWidget *page, + gint num, + gpointer data) +{ + ESendOptionsDialog *sod = data; + ESendOptionsDialogPrivate *priv = sod->priv; + + e_send_options_get_widgets_data (sod); + if (num > 0) { + GtkWidget *child; + + if (num == 1) { + gtk_widget_hide (priv->accepted_label); + gtk_widget_hide (priv->when_accepted); + gtk_widget_hide (priv->completed_label); + gtk_widget_hide (priv->when_completed); + gtk_widget_set_sensitive (priv->autodelete, TRUE); + sod->data->sopts = sod->data->mopts; + + child = gtk_notebook_get_nth_page (notebook, 1); + if (child != priv->status && (!GTK_IS_BIN (child) || gtk_bin_get_child (GTK_BIN (child)) != priv->status)) + gtk_widget_reparent (priv->status, child); + } else if (num == 2) { + gtk_widget_hide (priv->completed_label); + gtk_widget_hide (priv->when_completed); + gtk_widget_set_sensitive (priv->autodelete, FALSE); + + gtk_widget_show (priv->accepted_label); + gtk_widget_show (priv->when_accepted); + sod->data->sopts = sod->data->copts; + + child = gtk_notebook_get_nth_page (notebook, 2); + if (gtk_bin_get_child (GTK_BIN (child)) != priv->status) + gtk_widget_reparent (priv->status, child); + } else { + gtk_widget_set_sensitive (priv->autodelete, FALSE); + + gtk_widget_show (priv->completed_label); + gtk_widget_show (priv->when_completed); + gtk_widget_show (priv->accepted_label); + gtk_widget_show (priv->when_accepted); + sod->data->sopts = sod->data->topts; + + child = gtk_notebook_get_nth_page (notebook, 3); + if (gtk_bin_get_child (GTK_BIN (child)) != priv->status) + gtk_widget_reparent (priv->status, child); + } + } + e_send_options_fill_widgets_with_data (sod); +} + +static void +init_widgets (ESendOptionsDialog *sod) +{ + ESendOptionsDialogPrivate *priv; + + priv = sod->priv; + + g_signal_connect ( + priv->expiration, "toggled", + G_CALLBACK (expiration_toggled_cb), sod); + g_signal_connect ( + priv->reply_request, "toggled", + G_CALLBACK (reply_request_toggled_cb), sod); + g_signal_connect ( + priv->delay_delivery, "toggled", + G_CALLBACK (delay_delivery_toggled_cb), sod); + g_signal_connect ( + priv->create_sent, "toggled", + G_CALLBACK (sent_item_toggled_cb), sod); + + g_signal_connect ( + priv->main, "response", + G_CALLBACK (e_send_options_cb), sod); + g_signal_connect ( + priv->delay_until, "changed", + G_CALLBACK (delay_until_date_changed_cb), sod); + + if (priv->global) + g_signal_connect ( + priv->notebook, "switch-page", + G_CALLBACK (page_changed_cb), sod); + +} + +static gboolean +get_widgets (ESendOptionsDialog *sod) +{ + ESendOptionsDialogPrivate *priv; + GtkBuilder *builder; + + priv = sod->priv; + builder = sod->priv->builder; + + priv->main = e_builder_get_widget (builder, "send-options-dialog"); + if (!priv->main) + return FALSE; + + priv->priority = e_builder_get_widget (builder, "combo-priority"); + priv->status = e_builder_get_widget (builder, "status-tracking"); + priv->security = e_builder_get_widget (builder, "security-combo"); + priv->notebook = (GtkNotebook *) e_builder_get_widget (builder, "notebook"); + priv->reply_request = e_builder_get_widget (builder, "reply-request-button"); + priv->reply_convenient = e_builder_get_widget (builder, "reply-convinient"); + priv->reply_within = e_builder_get_widget (builder, "reply-within"); + priv->within_days = e_builder_get_widget (builder, "within-days"); + priv->delay_delivery = e_builder_get_widget (builder, "delay-delivery-button"); + priv->delay_until = e_builder_get_widget (builder, "until-date"); + gtk_widget_show (priv->delay_until); + priv->expiration = e_builder_get_widget (builder, "expiration-button"); + priv->expire_after = e_builder_get_widget (builder, "expire-after"); + priv->create_sent = e_builder_get_widget (builder, "create-sent-button"); + priv->delivered = e_builder_get_widget (builder, "delivered"); + priv->delivered_opened = e_builder_get_widget (builder, "delivered-opened"); + priv->all_info = e_builder_get_widget (builder, "all-info"); + priv->autodelete = e_builder_get_widget (builder, "autodelete"); + priv->when_opened = e_builder_get_widget (builder, "open-combo"); + priv->when_declined = e_builder_get_widget (builder, "delete-combo"); + priv->when_accepted = e_builder_get_widget (builder, "accept-combo"); + priv->when_completed = e_builder_get_widget (builder, "complete-combo"); + priv->security_label = e_builder_get_widget (builder, "security-label"); + priv->gopts_label = e_builder_get_widget (builder, "gopts-label"); + priv->priority_label = e_builder_get_widget (builder, "priority-label"); + priv->until_label = e_builder_get_widget (builder, "until-label"); + priv->opened_label = e_builder_get_widget (builder, "opened-label"); + priv->declined_label = e_builder_get_widget (builder, "declined-label"); + priv->accepted_label = e_builder_get_widget (builder, "accepted-label"); + priv->completed_label = e_builder_get_widget (builder, "completed-label"); + + return (priv->priority + && priv->security + && priv->status + && priv->reply_request + && priv->reply_convenient + && priv->reply_within + && priv->within_days + && priv->delay_delivery + && priv->delay_until + && priv->expiration + && priv->expire_after + && priv->create_sent + && priv->delivered + && priv->delivered_opened + && priv->autodelete + && priv->all_info + && priv->when_opened + && priv->when_declined + && priv->when_accepted + && priv->when_completed + && priv->security_label + && priv->priority_label + && priv->opened_label + && priv->gopts_label + && priv->declined_label + && priv->accepted_label + && priv->completed_label); + +} + +static void +setup_widgets (ESendOptionsDialog *sod, + Item_type type) +{ + ESendOptionsDialogPrivate *priv; + + priv = sod->priv; + + if (!priv->gopts_needed) { + gtk_notebook_set_show_tabs (priv->notebook, FALSE); + gtk_notebook_set_current_page (priv->notebook, 1); + gtk_widget_hide (priv->delay_until); + } else + gtk_notebook_set_show_tabs (priv->notebook, TRUE); + + gtk_label_set_mnemonic_widget (GTK_LABEL (priv->priority_label), priv->priority); + gtk_label_set_mnemonic_widget (GTK_LABEL (priv->security_label), priv->security); + gtk_label_set_mnemonic_widget (GTK_LABEL (priv->accepted_label), priv->when_accepted); + gtk_label_set_mnemonic_widget (GTK_LABEL (priv->declined_label), priv->when_declined); + gtk_label_set_mnemonic_widget (GTK_LABEL (priv->opened_label), priv->when_opened); + gtk_label_set_mnemonic_widget (GTK_LABEL (priv->completed_label), priv->when_completed); + gtk_label_set_mnemonic_widget (GTK_LABEL (priv->until_label), priv->delay_until); + + if (priv->global) { + GtkWidget *widget, *page; + + widget = gtk_label_new (_("Mail")); + page = gtk_alignment_new (0.0, 0.0, 0.0, 0.0); + gtk_widget_reparent (priv->status, page); + gtk_notebook_append_page (priv->notebook, page, widget); + gtk_container_child_set (GTK_CONTAINER (priv->notebook), page, "tab-fill", FALSE, "tab-expand", FALSE, NULL); + gtk_widget_show (page); + gtk_widget_show (widget); + + widget = gtk_label_new (_("Calendar")); + page = gtk_alignment_new (0.0, 0.0, 0.0, 0.0); + gtk_notebook_append_page (priv->notebook, page, widget); + gtk_container_child_set (GTK_CONTAINER (priv->notebook), page, "tab-fill", FALSE, "tab-expand", FALSE, NULL); + gtk_widget_show (page); + gtk_widget_show (widget); + + widget = gtk_label_new (_("Task")); + page = gtk_alignment_new (0.0, 0.0, 0.0, 0.0); + gtk_notebook_append_page (priv->notebook, page, widget); + gtk_container_child_set (GTK_CONTAINER (priv->notebook), page, "tab-fill", FALSE, "tab-expand", FALSE, NULL); + gtk_widget_show (page); + gtk_widget_show (widget); + + gtk_notebook_set_show_tabs (priv->notebook, TRUE); + } + + switch (type) { + case E_ITEM_MAIL: + priv->help_section = g_strdup ("groupwise-placeholder"); + gtk_widget_hide (priv->accepted_label); + gtk_widget_hide (priv->when_accepted); + gtk_widget_hide (priv->completed_label); + gtk_widget_hide (priv->when_completed); + gtk_label_set_text_with_mnemonic (GTK_LABEL (priv->declined_label), (_("When de_leted:"))); + break; + case E_ITEM_CALENDAR: + priv->help_section = g_strdup ("groupwise-placeholder"); + gtk_widget_hide (priv->completed_label); + gtk_widget_hide (priv->when_completed); + case E_ITEM_TASK: + priv->help_section = g_strdup ("groupwise-placeholder"); + gtk_widget_hide (priv->security_label); + gtk_widget_hide (priv->security); + gtk_widget_set_sensitive (priv->autodelete, FALSE); + break; + default: + break; + } +} + +ESendOptionsDialog * +e_send_options_dialog_new (void) +{ + ESendOptionsDialog *sod; + + sod = g_object_new (E_TYPE_SEND_OPTIONS_DIALOG, NULL); + + return sod; +} + +void +e_send_options_set_need_general_options (ESendOptionsDialog *sod, + gboolean needed) +{ + g_return_if_fail (E_IS_SEND_OPTIONS_DIALOG (sod)); + + sod->priv->gopts_needed = needed; +} + +gboolean +e_send_options_get_need_general_options (ESendOptionsDialog *sod) +{ + g_return_val_if_fail (E_IS_SEND_OPTIONS_DIALOG (sod), FALSE); + + return sod->priv->gopts_needed; +} + +gboolean +e_send_options_set_global (ESendOptionsDialog *sod, + gboolean set) +{ + g_return_val_if_fail (E_IS_SEND_OPTIONS_DIALOG (sod), FALSE); + + sod->priv->global = set; + + return TRUE; +} + +static void +e_send_options_cb (GtkDialog *dialog, + gint state, + gpointer func_data) +{ + ESendOptionsDialogPrivate *priv; + ESendOptionsDialog *sod; + + sod = func_data; + priv = sod->priv; + + switch (state) { + case GTK_RESPONSE_OK: + e_send_options_get_widgets_data (sod); + case GTK_RESPONSE_CANCEL: + gtk_widget_hide (priv->main); + gtk_widget_destroy (priv->main); + g_object_unref (priv->builder); + break; + case GTK_RESPONSE_HELP: + e_display_help ( + GTK_WINDOW (priv->main), + priv->help_section); + break; + } + + g_signal_emit (func_data, signals[SOD_RESPONSE], 0, state); +} + +gboolean +e_send_options_dialog_run (ESendOptionsDialog *sod, + GtkWidget *parent, + Item_type type) +{ + ESendOptionsDialogPrivate *priv; + GtkWidget *toplevel; + + g_return_val_if_fail (sod != NULL || E_IS_SEND_OPTIONS_DIALOG (sod), FALSE); + + priv = sod->priv; + + /* Make sure our custom widget classes are registered with + * GType before we load the GtkBuilder definition file. */ + E_TYPE_DATE_EDIT; + + priv->builder = gtk_builder_new (); + e_load_ui_builder_definition (priv->builder, "e-send-options.ui"); + + if (!get_widgets (sod)) { + g_object_unref (priv->builder); + g_message (G_STRLOC ": Could not get the Widgets \n"); + return FALSE; + } + + if (priv->global) { + g_free (sod->data->sopts); + sod->data->sopts = sod->data->mopts; + } + + setup_widgets (sod, type); + + toplevel = gtk_widget_get_toplevel (priv->main); + if (parent) + gtk_window_set_transient_for (GTK_WINDOW (toplevel), + GTK_WINDOW (parent)); + + e_send_options_fill_widgets_with_data (sod); + sensitize_widgets (sod); + init_widgets (sod); + gtk_window_set_modal ((GtkWindow *) priv->main, TRUE); + + gtk_widget_show (priv->main); + + return TRUE; +} + +static void +e_send_options_dialog_finalize (GObject *object) +{ + ESendOptionsDialog *sod; + + sod = E_SEND_OPTIONS_DIALOG (object); + + g_free (sod->priv->help_section); + + g_free (sod->data->gopts); + + if (!sod->priv->global) + g_free (sod->data->sopts); + + g_free (sod->data->mopts); + g_free (sod->data->copts); + g_free (sod->data->topts); + g_free (sod->data); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_send_options_dialog_parent_class)->finalize (object); +} + +/* Object initialization function for the task page */ +static void +e_send_options_dialog_init (ESendOptionsDialog *sod) +{ + ESendOptionsData *new; + + new = g_new0 (ESendOptionsData, 1); + new->gopts = g_new0 (ESendOptionsGeneral, 1); + new->sopts = g_new0 (ESendOptionsStatusTracking, 1); + new->mopts = g_new0 (ESendOptionsStatusTracking, 1); + new->copts = g_new0 (ESendOptionsStatusTracking, 1); + new->topts = g_new0 (ESendOptionsStatusTracking, 1); + + sod->priv = E_SEND_OPTIONS_DIALOG_GET_PRIVATE (sod); + + sod->data = new; + sod->data->initialized = FALSE; + sod->data->gopts->security = 0; + + sod->priv->gopts_needed = TRUE; +} + +/* Class initialization function for the Send Options */ +static void +e_send_options_dialog_class_init (ESendOptionsDialogClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ESendOptionsDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = e_send_options_dialog_finalize; + + signals[SOD_RESPONSE] = g_signal_new ( + "sod_response", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (ESendOptionsDialogClass, sod_response), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + +} diff --git a/e-util/e-send-options.h b/e-util/e-send-options.h new file mode 100644 index 0000000000..2cd8336f3c --- /dev/null +++ b/e-util/e-send-options.h @@ -0,0 +1,131 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __E_SEND_OPTIONS_DIALOG_H__ +#define __E_SEND_OPTIONS_DIALOG_H__ + +#include <gtk/gtk.h> +#include <time.h> + +#define E_TYPE_SEND_OPTIONS_DIALOG (e_send_options_dialog_get_type ()) +#define E_SEND_OPTIONS_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_TYPE_SEND_OPTIONS_DIALOG, ESendOptionsDialog)) +#define E_SEND_OPTIONS_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), E_TYPE_SEND_OPTIONS_DIALOG, ESendOptionsDialogClass)) +#define E_IS_SEND_OPTIONS_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_SEND_OPTIONS_DIALOG)) +#define E_IS_SEND_OPTIONS_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), E_TYPE_SEND_OPTIONS_DIALOG)) + +typedef struct _ESendOptionsDialog ESendOptionsDialog; +typedef struct _ESendOptionsDialogClass ESendOptionsDialogClass; +typedef struct _ESendOptionsDialogPrivate ESendOptionsDialogPrivate; + +typedef enum { + E_ITEM_NONE, + E_ITEM_MAIL, + E_ITEM_CALENDAR, + E_ITEM_TASK +} Item_type; + +typedef enum { + E_PRIORITY_UNDEFINED, + E_PRIORITY_HIGH, + E_PRIORITY_STANDARD, + E_PRIORITY_LOW +} ESendOptionsPriority; + +typedef enum { + E_SECURITY_NORMAL, + E_SECURITY_PROPRIETARY, + E_SECURITY_CONFIDENTIAL, + E_SECURITY_SECRET, + E_SECURITY_TOP_SECRET, + E_SECURITY_FOR_YOUR_EYES_ONLY +} ESendOptionsSecurity; + +typedef enum { + E_RETURN_NOTIFY_NONE, + E_RETURN_NOTIFY_MAIL +} ESendOptionsReturnNotify; + +typedef enum { + E_DELIVERED = 1, + E_DELIVERED_OPENED = 2, + E_ALL = 3 +} TrackInfo; + +typedef struct { + ESendOptionsPriority priority; + gint classify; + gboolean reply_enabled; + gboolean reply_convenient; + gint reply_within; + gboolean expiration_enabled; + gint expire_after; + gboolean delay_enabled; + time_t delay_until; + gint security; +} ESendOptionsGeneral; + +typedef struct { + gboolean tracking_enabled; + TrackInfo track_when; + gboolean autodelete; + ESendOptionsReturnNotify opened; + ESendOptionsReturnNotify accepted; + ESendOptionsReturnNotify declined; + ESendOptionsReturnNotify completed; +} ESendOptionsStatusTracking; + +typedef struct { + gboolean initialized; + + ESendOptionsGeneral *gopts; + ESendOptionsStatusTracking *sopts; + ESendOptionsStatusTracking *mopts; + ESendOptionsStatusTracking *copts; + ESendOptionsStatusTracking *topts; + +} ESendOptionsData; + +struct _ESendOptionsDialog { + GObject object; + + ESendOptionsData *data; + /* Private data */ + ESendOptionsDialogPrivate *priv; +}; + +struct _ESendOptionsDialogClass { + GObjectClass parent_class; + void (* sod_response) (ESendOptionsDialog *sd, gint status); +}; + +GType e_send_options_dialog_get_type (void); +ESendOptionsDialog *e_send_options_dialog_new (void); +void e_send_options_set_need_general_options (ESendOptionsDialog *sod, gboolean needed); +gboolean e_send_options_get_need_general_options (ESendOptionsDialog *sod); +gboolean e_send_options_dialog_run (ESendOptionsDialog *sod, GtkWidget *parent, Item_type type); +gboolean e_send_options_set_global (ESendOptionsDialog *sod, gboolean set); +#endif diff --git a/e-util/e-send-options.ui b/e-util/e-send-options.ui new file mode 100644 index 0000000000..681e6e0693 --- /dev/null +++ b/e-util/e-send-options.ui @@ -0,0 +1,949 @@ +<?xml version="1.0"?> +<interface> + <object class="GtkAdjustment" id="adjustment1"> + <property name="upper">100</property> + <property name="lower">0</property> + <property name="page_increment">10</property> + <property name="step_increment">1</property> + <property name="page_size">0</property> + <property name="value">5</property> + </object> + <object class="GtkAdjustment" id="adjustment2"> + <property name="upper">100</property> + <property name="lower">0</property> + <property name="page_increment">10</property> + <property name="step_increment">1</property> + <property name="page_size">0</property> + <property name="value">2</property> + </object> + <object class="GtkListStore" id="model1"> + <columns> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">Undefined</col> + </row> + <row> + <col id="0" translatable="yes">High</col> + </row> + <row> + <col id="0" translatable="yes">Standard</col> + </row> + <row> + <col id="0" translatable="yes">Low</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model2"> + <columns> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">Normal</col> + </row> + <row> + <col id="0" translatable="yes">Proprietary</col> + </row> + <row> + <col id="0" translatable="yes">Confidential</col> + </row> + <row> + <col id="0" translatable="yes">Secret</col> + </row> + <row> + <col id="0" translatable="yes">Top Secret</col> + </row> + <row> + <col id="0" translatable="yes">For Your Eyes Only</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model3"> + <columns> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" context="send-options" comments="Translators: Used in send options dialog">None</col> + </row> + <row> + <col id="0" translatable="yes">Mail Receipt</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model4"> + <columns> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" context="send-options">None</col> + </row> + <row> + <col id="0" translatable="yes">Mail Receipt</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model5"> + <columns> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" context="send-options">None</col> + </row> + <row> + <col id="0" translatable="yes">Mail Receipt</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model6"> + <columns> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" context="send-options">None</col> + </row> + <row> + <col id="0" translatable="yes">Mail Receipt</col> + </row> + </data> + </object> + <!-- interface-requires gtk+ 2.16 --> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkDialog" id="send-options-dialog"> + <property name="visible">True</property> + <property name="can_default">True</property> + <property name="title" translatable="yes">Send Options</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="window_position">mouse</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="vbox"> + <property name="visible">True</property> + <property name="can_default">True</property> + <child> + <object class="GtkNotebook" id="notebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkTable" id="general-options"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="n_rows">4</property> + <property name="n_columns">4</property> + <property name="column_spacing">12</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkLabel" id="label92"> + <property name="visible">True</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">4</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame16"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkTable" id="table32"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="n_rows">2</property> + <property name="n_columns">3</property> + <property name="column_spacing">12</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkCheckButton" id="reply-request-button"> + <property name="label" translatable="yes">R_eply requested</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="right_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment38"> + <property name="visible">True</property> + <property name="xscale">0</property> + <property name="left_padding">12</property> + <child> + <object class="GtkTable" id="table33"> + <property name="visible">True</property> + <property name="n_rows">2</property> + <property name="n_columns">3</property> + <property name="column_spacing">12</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkRadioButton" id="reply-within"> + <property name="label" translatable="yes" comments="Translators: This is part of 'Within [ X ] days', where [ X ] is a spinner with a number" context="ESendOptionsWithin">Wi_thin</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="within-days"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment1</property> + <property name="climb_rate">1</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label93"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: This is part of 'Within [ X ] days', where [ X ] is a spinner with a number" context="ESendOptionsWithin">days</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="reply-convinient"> + <property name="label" translatable="yes">_When convenient</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">reply-within</property> + </object> + <packing> + <property name="right_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options"/> + <property name="y_options"/> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label94"> + <property name="visible">True</property> + <property name="label" translatable="yes">Replies</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="right_attach">4</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame17"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkTable" id="table34"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="n_rows">4</property> + <property name="n_columns">2</property> + <child> + <object class="GtkCheckButton" id="delay-delivery-button"> + <property comments="To translators: This means Delay the message delivery for some time" name="label" translatable="yes">_Delay message delivery</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label95"> + <property name="visible">True</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment39"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <object class="GtkHBox" id="hbox11"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="label96"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: This is part of 'After [ X ] days', where [ X ] is a spinner with a number" context="ESendOptionsAfter">_After</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">expire-after</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="expire-after"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment2</property> + <property name="climb_rate">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label97"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: This is part of 'After [ X ] days', where [ X ] is a spinner with a number" context="ESendOptionsAfter">days</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_options">GTK_EXPAND</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="expiration-button"> + <property name="label" translatable="yes">_Set expiration date</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label98"> + <property name="visible">True</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment40"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <object class="GtkHBox" id="hbox13"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="until-label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: This is part of 'Until [ date ]', where [ date ] is a date picker" context="ESendOptions">_Until</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">until-date</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="EDateEdit" id="until-date"> + <property name="visible">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"/> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label103"> + <property name="visible">True</property> + <property name="label" translatable="yes">Delivery Options</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="right_attach">4</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkComboBox" id="combo-priority"> + <property name="visible">True</property> + <property name="model">model1</property> + <child> + <object class="GtkCellRendererText" id="renderer1"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="priority-label"> + <property name="visible">True</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Priority:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">combo-priority</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="security-label"> + <property name="visible">True</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Classification:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">security-combo</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkComboBox" id="security-combo"> + <property name="visible">True</property> + <property name="model">model2</property> + <child> + <object class="GtkCellRendererText" id="renderer2"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="gopts-label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Gene_ral Options</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="status-tracking"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkFrame" id="frame18"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment42"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <object class="GtkVBox" id="vbox10"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkCheckButton" id="create-sent-button"> + <property name="label" translatable="yes">Creat_e a sent item to track information</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment46"> + <property name="visible">True</property> + <property name="left_padding">19</property> + <child> + <object class="GtkRadioButton" id="delivered"> + <property name="label" translatable="yes">_Delivered</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment47"> + <property name="visible">True</property> + <property name="left_padding">19</property> + <child> + <object class="GtkRadioButton" id="delivered-opened"> + <property name="label" translatable="yes">Deli_vered and opened</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">delivered</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment48"> + <property name="visible">True</property> + <property name="left_padding">19</property> + <child> + <object class="GtkRadioButton" id="all-info"> + <property name="label" translatable="yes">_All information</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">delivered</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment56"> + <property name="visible">True</property> + <property name="left_padding">19</property> + <child> + <object class="GtkCheckButton" id="autodelete"> + <property name="label" translatable="yes">A_uto-delete sent item</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label106"> + <property name="visible">True</property> + <property name="label" translatable="yes">Status Tracking</property> + <property name="use_underline">True</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame19"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment43"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <object class="GtkTable" id="table35"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="n_rows">4</property> + <property name="n_columns">2</property> + <property name="column_spacing">12</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkLabel" id="opened-label"> + <property name="visible">True</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_When opened:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">open-combo</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="declined-label"> + <property name="visible">True</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">When decli_ned:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">delete-combo</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="completed-label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">When co_mpleted:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">complete-combo</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="accepted-label"> + <property name="visible">True</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">When acce_pted:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">accept-combo</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkComboBox" id="open-combo"> + <property name="visible">True</property> + <property name="model">model3</property> + <child> + <object class="GtkCellRendererText" id="renderer3"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="delete-combo"> + <property name="visible">True</property> + <property name="model">model4</property> + <child> + <object class="GtkCellRendererText" id="renderer4"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="accept-combo"> + <property name="visible">True</property> + <property name="model">model5</property> + <child> + <object class="GtkCellRendererText" id="renderer5"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="complete-combo"> + <property name="visible">True</property> + <property name="model">model6</property> + <child> + <object class="GtkCellRendererText" id="renderer6"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label111"> + <property name="visible">True</property> + <property name="label" translatable="yes">Return Notification</property> + <property name="use_underline">True</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="slabel"> + <property name="visible">True</property> + <property name="label" translatable="yes">Sta_tus Tracking</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="helpbutton1"> + <property name="label">gtk-help</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="cancelbutton1"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="okbutton1"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-11">helpbutton1</action-widget> + <action-widget response="-6">cancelbutton1</action-widget> + <action-widget response="-5">okbutton1</action-widget> + </action-widgets> + </object> +</interface> diff --git a/e-util/e-sorter-array.c b/e-util/e-sorter-array.c index 4caa092704..1c373160df 100644 --- a/e-util/e-sorter-array.c +++ b/e-util/e-sorter-array.c @@ -28,7 +28,8 @@ #include <string.h> #include "e-sorter-array.h" -#include "e-util.h" + +#include "e-misc-utils.h" #define d(x) diff --git a/e-util/e-sorter-array.h b/e-util/e-sorter-array.h index 5fb9c31f6b..07a32b4b82 100644 --- a/e-util/e-sorter-array.h +++ b/e-util/e-sorter-array.h @@ -20,6 +20,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef _E_SORTER_ARRAY_H_ #define _E_SORTER_ARRAY_H_ diff --git a/e-util/e-sorter.c b/e-util/e-sorter.c index b55d985daa..ecb597a832 100644 --- a/e-util/e-sorter.c +++ b/e-util/e-sorter.c @@ -28,7 +28,6 @@ #include <string.h> #include "e-sorter.h" -#include "e-util.h" #define d(x) diff --git a/e-util/e-sorter.h b/e-util/e-sorter.h index 37015e54ae..94b63f3bd4 100644 --- a/e-util/e-sorter.h +++ b/e-util/e-sorter.h @@ -21,6 +21,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef _E_SORTER_H_ #define _E_SORTER_H_ diff --git a/e-util/e-source-combo-box.c b/e-util/e-source-combo-box.c new file mode 100644 index 0000000000..d8d2273527 --- /dev/null +++ b/e-util/e-source-combo-box.c @@ -0,0 +1,701 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-source-combo-box.c + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-source-combo-box.h" +#include "e-cell-renderer-color.h" + +#define E_SOURCE_COMBO_BOX_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SOURCE_COMBO_BOX, ESourceComboBoxPrivate)) + +struct _ESourceComboBoxPrivate { + ESourceRegistry *registry; + gchar *extension_name; + + gulong source_added_handler_id; + gulong source_removed_handler_id; + gulong source_enabled_handler_id; + gulong source_disabled_handler_id; + + gboolean show_colors; +}; + +enum { + PROP_0, + PROP_EXTENSION_NAME, + PROP_REGISTRY, + PROP_SHOW_COLORS +}; + +enum { + COLUMN_COLOR, /* GDK_TYPE_COLOR */ + COLUMN_NAME, /* G_TYPE_STRING */ + COLUMN_SENSITIVE, /* G_TYPE_BOOLEAN */ + COLUMN_UID, /* G_TYPE_STRING */ + NUM_COLUMNS +}; + +G_DEFINE_TYPE (ESourceComboBox, e_source_combo_box, GTK_TYPE_COMBO_BOX) + +static gboolean +source_combo_box_traverse (GNode *node, + ESourceComboBox *combo_box) +{ + ESource *source; + ESourceSelectable *extension = NULL; + GtkTreeModel *model; + GtkTreeIter iter; + GString *indented; + GdkColor color; + const gchar *ext_name; + const gchar *display_name; + const gchar *uid; + gboolean sensitive = FALSE; + gboolean use_color = FALSE; + guint depth; + + /* Skip the root node. */ + if (G_NODE_IS_ROOT (node)) + return FALSE; + + ext_name = e_source_combo_box_get_extension_name (combo_box); + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo_box)); + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + + source = E_SOURCE (node->data); + uid = e_source_get_uid (source); + display_name = e_source_get_display_name (source); + + indented = g_string_new (NULL); + + depth = g_node_depth (node); + g_warn_if_fail (depth > 1); + while (--depth > 1) + g_string_append (indented, " "); + g_string_append (indented, display_name); + + if (ext_name != NULL && e_source_has_extension (source, ext_name)) { + extension = e_source_get_extension (source, ext_name); + sensitive = TRUE; + } + + if (E_IS_SOURCE_SELECTABLE (extension)) { + const gchar *color_spec; + + color_spec = e_source_selectable_get_color (extension); + if (color_spec != NULL && *color_spec != '\0') + use_color = gdk_color_parse (color_spec, &color); + } + + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + COLUMN_COLOR, use_color ? &color : NULL, + COLUMN_NAME, indented->str, + COLUMN_SENSITIVE, sensitive, + COLUMN_UID, uid, + -1); + + g_string_free (indented, TRUE); + + return FALSE; +} + +static void +source_combo_box_build_model (ESourceComboBox *combo_box) +{ + ESourceRegistry *registry; + GtkComboBox *gtk_combo_box; + GtkTreeModel *model; + GNode *root; + const gchar *active_id; + const gchar *extension_name; + + registry = e_source_combo_box_get_registry (combo_box); + extension_name = e_source_combo_box_get_extension_name (combo_box); + + gtk_combo_box = GTK_COMBO_BOX (combo_box); + model = gtk_combo_box_get_model (gtk_combo_box); + + /* Constructor properties trigger this function before the + * list store is configured. Detect it and return silently. */ + if (model == NULL) + return; + + /* Remember the active ID so we can try to restore it. */ + active_id = gtk_combo_box_get_active_id (gtk_combo_box); + + gtk_list_store_clear (GTK_LIST_STORE (model)); + + /* If we have no registry, leave the combo box empty. */ + if (registry == NULL) + return; + + /* If we have no extension name, leave the combo box empty. */ + if (extension_name == NULL) + return; + + root = e_source_registry_build_display_tree (registry, extension_name); + + g_node_traverse ( + root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + (GNodeTraverseFunc) source_combo_box_traverse, + combo_box); + + e_source_registry_free_display_tree (root); + + /* Restore the active ID, or else set it to something reasonable. */ + gtk_combo_box_set_active_id (gtk_combo_box, active_id); + if (gtk_combo_box_get_active_id (gtk_combo_box) == NULL) { + ESource *source; + + source = e_source_registry_ref_default_for_extension_name ( + registry, extension_name); + if (source != NULL) { + e_source_combo_box_set_active (combo_box, source); + g_object_unref (source); + } + } +} + +static void +source_combo_box_source_added_cb (ESourceRegistry *registry, + ESource *source, + ESourceComboBox *combo_box) +{ + source_combo_box_build_model (combo_box); +} + +static void +source_combo_box_source_removed_cb (ESourceRegistry *registry, + ESource *source, + ESourceComboBox *combo_box) +{ + source_combo_box_build_model (combo_box); +} + +static void +source_combo_box_source_enabled_cb (ESourceRegistry *registry, + ESource *source, + ESourceComboBox *combo_box) +{ + source_combo_box_build_model (combo_box); +} + +static void +source_combo_box_source_disabled_cb (ESourceRegistry *registry, + ESource *source, + ESourceComboBox *combo_box) +{ + source_combo_box_build_model (combo_box); +} + +static void +source_combo_box_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_EXTENSION_NAME: + e_source_combo_box_set_extension_name ( + E_SOURCE_COMBO_BOX (object), + g_value_get_string (value)); + return; + + case PROP_REGISTRY: + e_source_combo_box_set_registry ( + E_SOURCE_COMBO_BOX (object), + g_value_get_object (value)); + return; + + case PROP_SHOW_COLORS: + e_source_combo_box_set_show_colors ( + E_SOURCE_COMBO_BOX (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_combo_box_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_EXTENSION_NAME: + g_value_set_string ( + value, + e_source_combo_box_get_extension_name ( + E_SOURCE_COMBO_BOX (object))); + return; + + case PROP_REGISTRY: + g_value_set_object ( + value, + e_source_combo_box_get_registry ( + E_SOURCE_COMBO_BOX (object))); + return; + + case PROP_SHOW_COLORS: + g_value_set_boolean ( + value, + e_source_combo_box_get_show_colors ( + E_SOURCE_COMBO_BOX (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_combo_box_dispose (GObject *object) +{ + ESourceComboBoxPrivate *priv; + + priv = E_SOURCE_COMBO_BOX_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_signal_handler_disconnect ( + priv->registry, + priv->source_added_handler_id); + g_signal_handler_disconnect ( + priv->registry, + priv->source_removed_handler_id); + g_signal_handler_disconnect ( + priv->registry, + priv->source_enabled_handler_id); + g_signal_handler_disconnect ( + priv->registry, + priv->source_disabled_handler_id); + g_object_unref (priv->registry); + priv->registry = NULL; + } + + /* Chain up to parent's "dispose" method. */ + G_OBJECT_CLASS (e_source_combo_box_parent_class)->dispose (object); +} + +static void +source_combo_box_finalize (GObject *object) +{ + ESourceComboBoxPrivate *priv; + + priv = E_SOURCE_COMBO_BOX_GET_PRIVATE (object); + + g_free (priv->extension_name); + + /* Chain up to parent's "finalize" method. */ + G_OBJECT_CLASS (e_source_combo_box_parent_class)->finalize (object); +} + +static void +source_combo_box_constructed (GObject *object) +{ + ESourceComboBox *combo_box; + GtkCellRenderer *renderer; + GtkCellLayout *layout; + GtkListStore *store; + + combo_box = E_SOURCE_COMBO_BOX (object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_source_combo_box_parent_class)->constructed (object); + + store = gtk_list_store_new ( + NUM_COLUMNS, + GDK_TYPE_COLOR, /* COLUMN_COLOR */ + G_TYPE_STRING, /* COLUMN_NAME */ + G_TYPE_BOOLEAN, /* COLUMN_SENSITIVE */ + G_TYPE_STRING); /* COLUMN_UID */ + gtk_combo_box_set_model ( + GTK_COMBO_BOX (combo_box), + GTK_TREE_MODEL (store)); + g_object_unref (store); + + gtk_combo_box_set_id_column (GTK_COMBO_BOX (combo_box), COLUMN_UID); + + layout = GTK_CELL_LAYOUT (combo_box); + + renderer = e_cell_renderer_color_new (); + gtk_cell_layout_pack_start (layout, renderer, FALSE); + gtk_cell_layout_set_attributes ( + layout, renderer, + "color", COLUMN_COLOR, + "sensitive", COLUMN_SENSITIVE, + NULL); + + g_object_bind_property ( + combo_box, "show-colors", + renderer, "visible", + G_BINDING_SYNC_CREATE); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (layout, renderer, TRUE); + gtk_cell_layout_set_attributes ( + layout, renderer, + "text", COLUMN_NAME, + "sensitive", COLUMN_SENSITIVE, + NULL); + + source_combo_box_build_model (combo_box); +} + +static void +e_source_combo_box_class_init (ESourceComboBoxClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + g_type_class_add_private (class, sizeof (ESourceComboBoxPrivate)); + + object_class->set_property = source_combo_box_set_property; + object_class->get_property = source_combo_box_get_property; + object_class->dispose = source_combo_box_dispose; + object_class->finalize = source_combo_box_finalize; + object_class->constructed = source_combo_box_constructed; + + g_object_class_install_property ( + object_class, + PROP_EXTENSION_NAME, + g_param_spec_string ( + "extension-name", + "Extension Name", + "ESource extension name to filter", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + /* XXX Don't use G_PARAM_CONSTRUCT_ONLY here. We need to allow + * for this class to be instantiated by a GtkBuilder with no + * special construct parameters, and then subsequently give + * it an ESourceRegistry. */ + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + "Data source registry", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_COLORS, + g_param_spec_boolean ( + "show-colors", + "Show Colors", + "Whether to show colors next to names", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_source_combo_box_init (ESourceComboBox *combo_box) +{ + combo_box->priv = E_SOURCE_COMBO_BOX_GET_PRIVATE (combo_box); + +} + +/** + * e_source_combo_box_new: + * @registry: an #ESourceRegistry, or %NULL + * @extension_name: an #ESource extension name + * + * Creates a new #ESourceComboBox widget that lets the user pick an #ESource + * from the provided #ESourceRegistry. The displayed sources are restricted + * to those which have an @extension_name extension. + * + * Returns: a new #ESourceComboBox + * + * Since: 2.22 + **/ +GtkWidget * +e_source_combo_box_new (ESourceRegistry *registry, + const gchar *extension_name) +{ + if (registry != NULL) + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + return g_object_new ( + E_TYPE_SOURCE_COMBO_BOX, "registry", registry, + "extension-name", extension_name, NULL); +} + +/** + * e_source_combo_box_get_registry: + * @combo_box: an #ESourceComboBox + * + * Returns the #ESourceRegistry used to populate @combo_box. + * + * Returns: the #ESourceRegistry, or %NULL + * + * Since: 3.6 + **/ +ESourceRegistry * +e_source_combo_box_get_registry (ESourceComboBox *combo_box) +{ + g_return_val_if_fail (E_IS_SOURCE_COMBO_BOX (combo_box), NULL); + + return combo_box->priv->registry; +} + +/** + * e_source_combo_box_set_registry: + * @combo_box: an #ESourceComboBox + * @registry: an #ESourceRegistry + * + * Sets the #ESourceRegistry used to populate @combo_box. + * + * This function is intended for cases where @combo_box is instantiated + * by a #GtkBuilder and has to be given an #ESourceRegistry after it is + * fully constructed. + * + * Since: 3.6 + **/ +void +e_source_combo_box_set_registry (ESourceComboBox *combo_box, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_COMBO_BOX (combo_box)); + + if (combo_box->priv->registry == registry) + return; + + if (registry != NULL) { + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_object_ref (registry); + } + + if (combo_box->priv->registry != NULL) { + g_signal_handler_disconnect ( + combo_box->priv->registry, + combo_box->priv->source_added_handler_id); + g_signal_handler_disconnect ( + combo_box->priv->registry, + combo_box->priv->source_removed_handler_id); + g_signal_handler_disconnect ( + combo_box->priv->registry, + combo_box->priv->source_enabled_handler_id); + g_signal_handler_disconnect ( + combo_box->priv->registry, + combo_box->priv->source_disabled_handler_id); + g_object_unref (combo_box->priv->registry); + } + + combo_box->priv->registry = registry; + + combo_box->priv->source_added_handler_id = 0; + combo_box->priv->source_removed_handler_id = 0; + combo_box->priv->source_enabled_handler_id = 0; + combo_box->priv->source_disabled_handler_id = 0; + + if (registry != NULL) { + gulong handler_id; + + handler_id = g_signal_connect ( + registry, "source-added", + G_CALLBACK (source_combo_box_source_added_cb), + combo_box); + combo_box->priv->source_added_handler_id = handler_id; + + handler_id = g_signal_connect ( + registry, "source-removed", + G_CALLBACK (source_combo_box_source_removed_cb), + combo_box); + combo_box->priv->source_removed_handler_id = handler_id; + + handler_id = g_signal_connect ( + registry, "source-enabled", + G_CALLBACK (source_combo_box_source_enabled_cb), + combo_box); + combo_box->priv->source_enabled_handler_id = handler_id; + + handler_id = g_signal_connect ( + registry, "source-disabled", + G_CALLBACK (source_combo_box_source_disabled_cb), + combo_box); + combo_box->priv->source_disabled_handler_id = handler_id; + } + + source_combo_box_build_model (combo_box); + + g_object_notify (G_OBJECT (combo_box), "registry"); +} + +/** + * e_source_combo_box_get_extension_name: + * @combo_box: an #ESourceComboBox + * + * Returns the extension name used to filter which data sources are + * shown in @combo_box. + * + * Returns: the #ESource extension name + * + * Since: 3.6 + **/ +const gchar * +e_source_combo_box_get_extension_name (ESourceComboBox *combo_box) +{ + g_return_val_if_fail (E_IS_SOURCE_COMBO_BOX (combo_box), NULL); + + return combo_box->priv->extension_name; +} + +/** + * e_source_combo_box_set_extension_name: + * @combo_box: an #ESourceComboBox + * @extension_name: an #ESource extension name + * + * Sets the extension name used to filter which data sources are shown in + * @combo_box. + * + * Since: 3.6 + **/ +void +e_source_combo_box_set_extension_name (ESourceComboBox *combo_box, + const gchar *extension_name) +{ + g_return_if_fail (E_IS_SOURCE_COMBO_BOX (combo_box)); + + if (g_strcmp0 (combo_box->priv->extension_name, extension_name) == 0) + return; + + g_free (combo_box->priv->extension_name); + combo_box->priv->extension_name = g_strdup (extension_name); + + source_combo_box_build_model (combo_box); + + g_object_notify (G_OBJECT (combo_box), "extension-name"); +} + +/** + * e_source_combo_box_get_show_colors: + * @combo_box: an #ESourceComboBox + * + * Returns whether colors are shown next to data sources. + * + * Returns: %TRUE if colors are being shown + * + * Since: 3.6 + **/ +gboolean +e_source_combo_box_get_show_colors (ESourceComboBox *combo_box) +{ + g_return_val_if_fail (E_IS_SOURCE_COMBO_BOX (combo_box), FALSE); + + return combo_box->priv->show_colors; +} + +/** + * e_source_combo_box_set_show_colors: + * @combo_box: an #ESourceComboBox + * @show_colors: whether to show colors + * + * Sets whether to show colors next to data sources. + * + * Since: 3.6 + **/ +void +e_source_combo_box_set_show_colors (ESourceComboBox *combo_box, + gboolean show_colors) +{ + g_return_if_fail (E_IS_SOURCE_COMBO_BOX (combo_box)); + + if ((show_colors ? 1 : 0) == (combo_box->priv->show_colors ? 1 : 0)) + return; + + combo_box->priv->show_colors = show_colors; + + source_combo_box_build_model (combo_box); + + g_object_notify (G_OBJECT (combo_box), "show-colors"); +} + +/** + * e_source_combo_box_ref_active: + * @combo_box: an #ESourceComboBox + * + * Returns the #ESource corresponding to the currently active item, + * or %NULL if there is no active item. + * + * The returned #ESource is referenced for thread-safety and must be + * unreferenced with g_object_unref() when finished with it. + * + * Returns: an #ESource or %NULL + * + * Since: 3.6 + **/ +ESource * +e_source_combo_box_ref_active (ESourceComboBox *combo_box) +{ + ESourceRegistry *registry; + GtkComboBox *gtk_combo_box; + const gchar *active_id; + + g_return_val_if_fail (E_IS_SOURCE_COMBO_BOX (combo_box), NULL); + + registry = e_source_combo_box_get_registry (combo_box); + + gtk_combo_box = GTK_COMBO_BOX (combo_box); + active_id = gtk_combo_box_get_active_id (gtk_combo_box); + + if (active_id == NULL) + return NULL; + + return e_source_registry_ref_source (registry, active_id); +} + +/** + * e_source_combo_box_set_active: + * @combo_box: an #ESourceComboBox + * @source: an #ESource + * + * Sets the active item to the one corresponding to @source. + * + * Since: 2.22 + **/ +void +e_source_combo_box_set_active (ESourceComboBox *combo_box, + ESource *source) +{ + GtkComboBox *gtk_combo_box; + const gchar *uid; + + g_return_if_fail (E_IS_SOURCE_COMBO_BOX (combo_box)); + g_return_if_fail (E_IS_SOURCE (source)); + + uid = e_source_get_uid (source); + + gtk_combo_box = GTK_COMBO_BOX (combo_box); + gtk_combo_box_set_active_id (gtk_combo_box, uid); +} + diff --git a/e-util/e-source-combo-box.h b/e-util/e-source-combo-box.h new file mode 100644 index 0000000000..d022f4a8ce --- /dev/null +++ b/e-util/e-source-combo-box.h @@ -0,0 +1,90 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-source-combo-box.h + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SOURCE_COMBO_BOX_H +#define E_SOURCE_COMBO_BOX_H + +#include <gtk/gtk.h> +#include <libedataserver/libedataserver.h> + +#define E_TYPE_SOURCE_COMBO_BOX \ + (e_source_combo_box_get_type ()) +#define E_SOURCE_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SOURCE_COMBO_BOX, ESourceComboBox)) +#define E_SOURCE_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SOURCE_COMBO_BOX, ESourceComboBoxClass)) +#define E_IS_SOURCE_COMBO_BOX(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_SOURCE_COMBO_BOX)) +#define E_IS_SOURCE_COMBO_BOX_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE ((cls), E_TYPE_SOURCE_COMBO_BOX)) +#define E_SOURCE_COMBO_BOX_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SOURCE_COMBO_BOX, ESourceComboBox)) + +G_BEGIN_DECLS + +typedef struct _ESourceComboBox ESourceComboBox; +typedef struct _ESourceComboBoxClass ESourceComboBoxClass; +typedef struct _ESourceComboBoxPrivate ESourceComboBoxPrivate; + +/** + * ESourceComboBox: + * + * Since: 2.22 + **/ +struct _ESourceComboBox { + GtkComboBox parent; + ESourceComboBoxPrivate *priv; +}; + +struct _ESourceComboBoxClass { + GtkComboBoxClass parent_class; +}; + +GType e_source_combo_box_get_type (void); +GtkWidget * e_source_combo_box_new (ESourceRegistry *registry, + const gchar *extension_name); +ESourceRegistry * + e_source_combo_box_get_registry (ESourceComboBox *combo_box); +void e_source_combo_box_set_registry (ESourceComboBox *combo_box, + ESourceRegistry *registry); +const gchar * e_source_combo_box_get_extension_name + (ESourceComboBox *combo_box); +void e_source_combo_box_set_extension_name + (ESourceComboBox *combo_box, + const gchar *extension_name); +gboolean e_source_combo_box_get_show_colors + (ESourceComboBox *combo_box); +void e_source_combo_box_set_show_colors + (ESourceComboBox *combo_box, + gboolean show_colors); +ESource * e_source_combo_box_ref_active (ESourceComboBox *combo_box); +void e_source_combo_box_set_active (ESourceComboBox *combo_box, + ESource *source); + +G_END_DECLS + +#endif /* E_SOURCE_COMBO_BOX_H */ diff --git a/e-util/e-source-config-backend.c b/e-util/e-source-config-backend.c new file mode 100644 index 0000000000..e6802f99ae --- /dev/null +++ b/e-util/e-source-config-backend.c @@ -0,0 +1,140 @@ +/* + * e-source-config-backend.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-source-config-backend.h" + +G_DEFINE_TYPE ( + ESourceConfigBackend, + e_source_config_backend, + E_TYPE_EXTENSION) + +static gboolean +source_config_backend_allow_creation (ESourceConfigBackend *backend) +{ + return TRUE; +} + +static void +source_config_backend_insert_widgets (ESourceConfigBackend *backend, + ESource *scratch_source) +{ + /* does nothing */ +} + +static gboolean +source_config_backend_check_complete (ESourceConfigBackend *backend, + ESource *scratch_source) +{ + return TRUE; +} + +static void +source_config_backend_commit_changes (ESourceConfigBackend *backend, + ESource *scratch_source) +{ + /* does nothing */ +} + +static void +e_source_config_backend_class_init (ESourceConfigBackendClass *class) +{ + EExtensionClass *extension_class; + + extension_class = E_EXTENSION_CLASS (class); + extension_class->extensible_type = E_TYPE_SOURCE_CONFIG; + + class->allow_creation = source_config_backend_allow_creation; + class->insert_widgets = source_config_backend_insert_widgets; + class->check_complete = source_config_backend_check_complete; + class->commit_changes = source_config_backend_commit_changes; +} + +static void +e_source_config_backend_init (ESourceConfigBackend *backend) +{ +} + +ESourceConfig * +e_source_config_backend_get_config (ESourceConfigBackend *backend) +{ + EExtensible *extensible; + + g_return_val_if_fail (E_IS_SOURCE_CONFIG_BACKEND (backend), NULL); + + extensible = e_extension_get_extensible (E_EXTENSION (backend)); + + return E_SOURCE_CONFIG (extensible); +} + +gboolean +e_source_config_backend_allow_creation (ESourceConfigBackend *backend) +{ + ESourceConfigBackendClass *class; + + g_return_val_if_fail (E_IS_SOURCE_CONFIG_BACKEND (backend), FALSE); + + class = E_SOURCE_CONFIG_BACKEND_GET_CLASS (backend); + g_return_val_if_fail (class->allow_creation != NULL, FALSE); + + return class->allow_creation (backend); +} + +void +e_source_config_backend_insert_widgets (ESourceConfigBackend *backend, + ESource *scratch_source) +{ + ESourceConfigBackendClass *class; + + g_return_if_fail (E_IS_SOURCE_CONFIG_BACKEND (backend)); + g_return_if_fail (E_IS_SOURCE (scratch_source)); + + class = E_SOURCE_CONFIG_BACKEND_GET_CLASS (backend); + g_return_if_fail (class->insert_widgets != NULL); + + class->insert_widgets (backend, scratch_source); +} + +gboolean +e_source_config_backend_check_complete (ESourceConfigBackend *backend, + ESource *scratch_source) +{ + ESourceConfigBackendClass *class; + + g_return_val_if_fail (E_IS_SOURCE_CONFIG_BACKEND (backend), FALSE); + g_return_val_if_fail (E_IS_SOURCE (scratch_source), FALSE); + + class = E_SOURCE_CONFIG_BACKEND_GET_CLASS (backend); + g_return_val_if_fail (class->check_complete != NULL, FALSE); + + return class->check_complete (backend, scratch_source); +} + +void +e_source_config_backend_commit_changes (ESourceConfigBackend *backend, + ESource *scratch_source) +{ + ESourceConfigBackendClass *class; + + g_return_if_fail (E_IS_SOURCE_CONFIG_BACKEND (backend)); + g_return_if_fail (E_IS_SOURCE (scratch_source)); + + class = E_SOURCE_CONFIG_BACKEND_GET_CLASS (backend); + g_return_if_fail (class->commit_changes != NULL); + + class->commit_changes (backend, scratch_source); +} diff --git a/e-util/e-source-config-backend.h b/e-util/e-source-config-backend.h new file mode 100644 index 0000000000..3191ca1c23 --- /dev/null +++ b/e-util/e-source-config-backend.h @@ -0,0 +1,98 @@ +/* + * e-source-config-backend.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SOURCE_CONFIG_BACKEND_H +#define E_SOURCE_CONFIG_BACKEND_H + +#include <libebackend/libebackend.h> + +#include <e-util/e-source-config.h> + +/* Standard GObject macros */ +#define E_TYPE_SOURCE_CONFIG_BACKEND \ + (e_source_config_backend_get_type ()) +#define E_SOURCE_CONFIG_BACKEND(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SOURCE_CONFIG_BACKEND, ESourceConfigBackend)) +#define E_SOURCE_CONFIG_BACKEND_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SOURCE_CONFIG_BACKEND, ESourceConfigBackendClass)) +#define E_IS_SOURCE_CONFIG_BACKEND(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SOURCE_CONFIG_BACKEND)) +#define E_IS_SOURCE_CONFIG_BACKEND_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SOURCE_CONFIG_BACKEND)) +#define E_SOURCE_CONFIG_BACKEND_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SOURCE_CONFIG_BACKEND, ESourceConfigBackendClass)) + +G_BEGIN_DECLS + +typedef struct _ESourceConfigBackend ESourceConfigBackend; +typedef struct _ESourceConfigBackendClass ESourceConfigBackendClass; +typedef struct _ESourceConfigBackendPrivate ESourceConfigBackendPrivate; + +struct _ESourceConfigBackend { + EExtension parent; + ESourceConfigBackendPrivate *priv; +}; + +struct _ESourceConfigBackendClass { + EExtensionClass parent_class; + + /* This should match backend names used in ESourceBackend. */ + const gchar *backend_name; + + /* Optional. Collection-based backends can leave this NULL. + * This is only for sources which have a fixed parent source, + * usually one of the "stub" placeholders ("local-stub", etc). */ + const gchar *parent_uid; + + gboolean (*allow_creation) (ESourceConfigBackend *backend); + void (*insert_widgets) (ESourceConfigBackend *backend, + ESource *scratch_source); + gboolean (*check_complete) (ESourceConfigBackend *backend, + ESource *scratch_source); + void (*commit_changes) (ESourceConfigBackend *backend, + ESource *scratch_source); +}; + +GType e_source_config_backend_get_type + (void) G_GNUC_CONST; +ESourceConfig * e_source_config_backend_get_config + (ESourceConfigBackend *backend); +gboolean e_source_config_backend_allow_creation + (ESourceConfigBackend *backend); +void e_source_config_backend_insert_widgets + (ESourceConfigBackend *backend, + ESource *scratch_source); +gboolean e_source_config_backend_check_complete + (ESourceConfigBackend *backend, + ESource *scratch_source); +void e_source_config_backend_commit_changes + (ESourceConfigBackend *backend, + ESource *scratch_source); + +G_END_DECLS + +#endif /* E_SOURCE_CONFIG_BACKEND_H */ diff --git a/e-util/e-source-config-dialog.c b/e-util/e-source-config-dialog.c new file mode 100644 index 0000000000..8a311c8ab1 --- /dev/null +++ b/e-util/e-source-config-dialog.c @@ -0,0 +1,394 @@ +/* + * e-source-config-dialog.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-source-config-dialog.h" + +#include "e-alert-bar.h" +#include "e-alert-dialog.h" +#include "e-alert-sink.h" + +#define E_SOURCE_CONFIG_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SOURCE_CONFIG_DIALOG, ESourceConfigDialogPrivate)) + +struct _ESourceConfigDialogPrivate { + ESourceConfig *config; + ESourceRegistry *registry; + + GtkWidget *alert_bar; + gulong alert_bar_visible_handler_id; +}; + +enum { + PROP_0, + PROP_CONFIG +}; + +/* Forward Declarations */ +static void e_source_config_dialog_alert_sink_init + (EAlertSinkInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + ESourceConfigDialog, + e_source_config_dialog, + GTK_TYPE_DIALOG, + G_IMPLEMENT_INTERFACE ( + E_TYPE_ALERT_SINK, + e_source_config_dialog_alert_sink_init)) + +static void +source_config_dialog_commit_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ESourceConfig *config; + ESourceConfigDialog *dialog; + GdkWindow *gdk_window; + GError *error = NULL; + + config = E_SOURCE_CONFIG (object); + dialog = E_SOURCE_CONFIG_DIALOG (user_data); + + /* Set the cursor back to normal. */ + gdk_window = gtk_widget_get_window (GTK_WIDGET (dialog)); + gdk_window_set_cursor (gdk_window, NULL); + + /* Allow user interaction with window content. */ + gtk_widget_set_sensitive (GTK_WIDGET (dialog), TRUE); + + e_source_config_commit_finish (config, result, &error); + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_object_unref (dialog); + g_error_free (error); + + } else if (error != NULL) { + e_alert_submit ( + E_ALERT_SINK (dialog), + "system:simple-error", + error->message, NULL); + g_object_unref (dialog); + g_error_free (error); + + } else { + gtk_widget_destroy (GTK_WIDGET (dialog)); + } +} + +static void +source_config_dialog_commit (ESourceConfigDialog *dialog) +{ + GdkCursor *gdk_cursor; + GdkWindow *gdk_window; + ESourceConfig *config; + + config = e_source_config_dialog_get_config (dialog); + + /* Clear any previous alerts. */ + e_alert_bar_clear (E_ALERT_BAR (dialog->priv->alert_bar)); + + /* Make the cursor appear busy. */ + gdk_cursor = gdk_cursor_new (GDK_WATCH); + gdk_window = gtk_widget_get_window (GTK_WIDGET (dialog)); + gdk_window_set_cursor (gdk_window, gdk_cursor); + g_object_unref (gdk_cursor); + + /* Prevent user interaction with window content. */ + gtk_widget_set_sensitive (GTK_WIDGET (dialog), FALSE); + + /* XXX This operation is not cancellable. */ + e_source_config_commit ( + config, NULL, + source_config_dialog_commit_cb, + g_object_ref (dialog)); +} + +static void +source_config_dialog_source_removed_cb (ESourceRegistry *registry, + ESource *removed_source, + ESourceConfigDialog *dialog) +{ + ESourceConfig *config; + ESource *original_source; + + /* If the ESource being edited is removed, cancel the dialog. */ + + config = e_source_config_dialog_get_config (dialog); + original_source = e_source_config_get_original_source (config); + + if (original_source == NULL) + return; + + if (!e_source_equal (original_source, removed_source)) + return; + + gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL); +} + +static void +source_config_alert_bar_visible_cb (EAlertBar *alert_bar, + GParamSpec *pspec, + ESourceConfigDialog *dialog) +{ + e_source_config_resize_window (dialog->priv->config); +} + +static void +source_config_dialog_set_config (ESourceConfigDialog *dialog, + ESourceConfig *config) +{ + ESourceRegistry *registry; + + g_return_if_fail (E_IS_SOURCE_CONFIG (config)); + g_return_if_fail (dialog->priv->config == NULL); + + dialog->priv->config = g_object_ref (config); + + registry = e_source_config_get_registry (config); + dialog->priv->registry = g_object_ref (registry); + + g_signal_connect ( + registry, "source-removed", + G_CALLBACK (source_config_dialog_source_removed_cb), dialog); +} + +static void +source_config_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CONFIG: + source_config_dialog_set_config ( + E_SOURCE_CONFIG_DIALOG (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_config_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CONFIG: + g_value_set_object ( + value, + e_source_config_dialog_get_config ( + E_SOURCE_CONFIG_DIALOG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_config_dialog_dispose (GObject *object) +{ + ESourceConfigDialogPrivate *priv; + + priv = E_SOURCE_CONFIG_DIALOG_GET_PRIVATE (object); + + if (priv->config != NULL) { + g_object_unref (priv->config); + priv->config = NULL; + } + + if (priv->registry != NULL) { + g_signal_handlers_disconnect_matched ( + priv->registry, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->alert_bar != NULL) { + g_signal_handler_disconnect ( + priv->alert_bar, + priv->alert_bar_visible_handler_id); + g_object_unref (priv->alert_bar); + priv->alert_bar = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_source_config_dialog_parent_class)->dispose (object); +} + +static void +source_config_dialog_constructed (GObject *object) +{ + ESourceConfigDialogPrivate *priv; + GtkWidget *content_area; + GtkWidget *config; + GtkWidget *widget; + gulong handler_id; + + priv = E_SOURCE_CONFIG_DIALOG_GET_PRIVATE (object); + + config = GTK_WIDGET (priv->config); + + widget = gtk_dialog_get_widget_for_response ( + GTK_DIALOG (object), GTK_RESPONSE_OK); + + gtk_container_set_border_width (GTK_CONTAINER (object), 5); + gtk_container_set_border_width (GTK_CONTAINER (config), 5); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (object)); + gtk_box_pack_start (GTK_BOX (content_area), config, TRUE, TRUE, 0); + gtk_widget_show (config); + + /* Don't use G_BINDING_SYNC_CREATE here. The ESourceConfig widget + * is not ready to run check_complete() until after it's realized. */ + g_object_bind_property ( + config, "complete", + widget, "sensitive", + G_BINDING_DEFAULT); + + widget = e_alert_bar_new (); + gtk_box_pack_start (GTK_BOX (content_area), widget, FALSE, FALSE, 0); + priv->alert_bar = g_object_ref (widget); + /* EAlertBar controls its own visibility. */ + + handler_id = g_signal_connect ( + priv->alert_bar, "notify::visible", + G_CALLBACK (source_config_alert_bar_visible_cb), object); + + priv->alert_bar_visible_handler_id = handler_id; +} + +static void +source_config_dialog_response (GtkDialog *dialog, + gint response_id) +{ + /* Do not chain up. GtkDialog does not implement this method. */ + + switch (response_id) { + case GTK_RESPONSE_OK: + source_config_dialog_commit ( + E_SOURCE_CONFIG_DIALOG (dialog)); + break; + case GTK_RESPONSE_CANCEL: + gtk_widget_destroy (GTK_WIDGET (dialog)); + break; + default: + break; + } +} + +static void +source_config_dialog_submit_alert (EAlertSink *alert_sink, + EAlert *alert) +{ + ESourceConfigDialogPrivate *priv; + EAlertBar *alert_bar; + GtkWidget *dialog; + GtkWindow *parent; + + priv = E_SOURCE_CONFIG_DIALOG_GET_PRIVATE (alert_sink); + + switch (e_alert_get_message_type (alert)) { + case GTK_MESSAGE_INFO: + case GTK_MESSAGE_WARNING: + case GTK_MESSAGE_ERROR: + alert_bar = E_ALERT_BAR (priv->alert_bar); + e_alert_bar_add_alert (alert_bar, alert); + break; + + default: + parent = GTK_WINDOW (alert_sink); + dialog = e_alert_dialog_new (parent, alert); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + break; + } +} + +static void +e_source_config_dialog_class_init (ESourceConfigDialogClass *class) +{ + GObjectClass *object_class; + GtkDialogClass *dialog_class; + + g_type_class_add_private (class, sizeof (ESourceConfigDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = source_config_dialog_set_property; + object_class->get_property = source_config_dialog_get_property; + object_class->dispose = source_config_dialog_dispose; + object_class->constructed = source_config_dialog_constructed; + + dialog_class = GTK_DIALOG_CLASS (class); + dialog_class->response = source_config_dialog_response; + + g_object_class_install_property ( + object_class, + PROP_CONFIG, + g_param_spec_object ( + "config", + "Config", + "The ESourceConfig instance", + E_TYPE_SOURCE_CONFIG, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_source_config_dialog_alert_sink_init (EAlertSinkInterface *interface) +{ + interface->submit_alert = source_config_dialog_submit_alert; +} + +static void +e_source_config_dialog_init (ESourceConfigDialog *dialog) +{ + dialog->priv = E_SOURCE_CONFIG_DIALOG_GET_PRIVATE (dialog); + + gtk_dialog_add_buttons ( + GTK_DIALOG (dialog), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, + NULL); + + gtk_dialog_set_default_response ( + GTK_DIALOG (dialog), GTK_RESPONSE_OK); +} + +GtkWidget * +e_source_config_dialog_new (ESourceConfig *config) +{ + g_return_val_if_fail (E_IS_SOURCE_CONFIG (config), NULL); + + return g_object_new ( + E_TYPE_SOURCE_CONFIG_DIALOG, + "config", config, NULL); +} + +ESourceConfig * +e_source_config_dialog_get_config (ESourceConfigDialog *dialog) +{ + g_return_val_if_fail (E_IS_SOURCE_CONFIG_DIALOG (dialog), NULL); + + return dialog->priv->config; +} diff --git a/e-util/e-source-config-dialog.h b/e-util/e-source-config-dialog.h new file mode 100644 index 0000000000..6f01c8a0eb --- /dev/null +++ b/e-util/e-source-config-dialog.h @@ -0,0 +1,69 @@ +/* + * e-source-config-dialog.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SOURCE_CONFIG_DIALOG_H +#define E_SOURCE_CONFIG_DIALOG_H + +#include <e-util/e-source-config.h> + +/* Standard GObject macros */ +#define E_TYPE_SOURCE_CONFIG_DIALOG \ + (e_source_config_dialog_get_type ()) +#define E_SOURCE_CONFIG_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SOURCE_CONFIG_DIALOG, ESourceConfigDialog)) +#define E_SOURCE_CONFIG_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SOURCE_CONFIG_DIALOG, ESourceConfigDialogClass)) +#define E_IS_SOURCE_CONFIG_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SOURCE_CONFIG_DIALOG)) +#define E_IS_SOURCE_CONFIG_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SOURCE_CONFIG_DIALOG)) +#define E_SOURCE_CONFIG_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SOURCE_CONFIG_DIALOG, ESourceConfigDialogClass)) + +G_BEGIN_DECLS + +typedef struct _ESourceConfigDialog ESourceConfigDialog; +typedef struct _ESourceConfigDialogClass ESourceConfigDialogClass; +typedef struct _ESourceConfigDialogPrivate ESourceConfigDialogPrivate; + +struct _ESourceConfigDialog { + GtkDialog parent; + ESourceConfigDialogPrivate *priv; +}; + +struct _ESourceConfigDialogClass { + GtkDialogClass parent_class; +}; + +GType e_source_config_dialog_get_type (void) G_GNUC_CONST; +GtkWidget * e_source_config_dialog_new (ESourceConfig *config); +ESourceConfig * e_source_config_dialog_get_config + (ESourceConfigDialog *dialog); + +G_END_DECLS + +#endif /* E_SOURCE_CONFIG_DIALOG_H */ diff --git a/e-util/e-source-config.c b/e-util/e-source-config.c new file mode 100644 index 0000000000..aacb48dd5c --- /dev/null +++ b/e-util/e-source-config.c @@ -0,0 +1,1447 @@ +/* + * e-source-config.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "e-source-config.h" + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include <libebackend/libebackend.h> + +#include "e-interval-chooser.h" +#include "e-marshal.h" +#include "e-source-config-backend.h" + +#define E_SOURCE_CONFIG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SOURCE_CONFIG, ESourceConfigPrivate)) + +typedef struct _Candidate Candidate; + +struct _ESourceConfigPrivate { + ESource *original_source; + ESource *collection_source; + ESourceRegistry *registry; + + GHashTable *backends; + GPtrArray *candidates; + + GtkWidget *type_label; + GtkWidget *type_combo; + GtkWidget *name_label; + GtkWidget *name_entry; + GtkWidget *backend_box; + GtkSizeGroup *size_group; + + gboolean complete; +}; + +struct _Candidate { + GtkWidget *page; + ESource *scratch_source; + ESourceConfigBackend *backend; + gulong changed_handler_id; +}; + +enum { + PROP_0, + PROP_COLLECTION_SOURCE, + PROP_COMPLETE, + PROP_ORIGINAL_SOURCE, + PROP_REGISTRY +}; + +enum { + CHECK_COMPLETE, + COMMIT_CHANGES, + INIT_CANDIDATE, + RESIZE_WINDOW, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE_WITH_CODE ( + ESourceConfig, + e_source_config, + GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +static void +source_config_init_backends (ESourceConfig *config) +{ + GList *list, *iter; + + config->priv->backends = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_object_unref); + + e_extensible_load_extensions (E_EXTENSIBLE (config)); + + list = e_extensible_list_extensions ( + E_EXTENSIBLE (config), E_TYPE_SOURCE_CONFIG_BACKEND); + + for (iter = list; iter != NULL; iter = g_list_next (iter)) { + ESourceConfigBackend *backend; + ESourceConfigBackendClass *class; + + backend = E_SOURCE_CONFIG_BACKEND (iter->data); + class = E_SOURCE_CONFIG_BACKEND_GET_CLASS (backend); + + if (class->backend_name != NULL) + g_hash_table_insert ( + config->priv->backends, + g_strdup (class->backend_name), + g_object_ref (backend)); + } + + g_list_free (list); +} + +static gint +source_config_compare_sources (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + ESource *source_a; + ESource *source_b; + ESource *parent_a; + ESource *parent_b; + ESourceConfig *config; + ESourceRegistry *registry; + const gchar *parent_uid_a; + const gchar *parent_uid_b; + gint result; + + source_a = E_SOURCE (a); + source_b = E_SOURCE (b); + config = E_SOURCE_CONFIG (user_data); + + if (e_source_equal (source_a, source_b)) + return 0; + + /* "On This Computer" always comes first. */ + + parent_uid_a = e_source_get_parent (source_a); + parent_uid_b = e_source_get_parent (source_b); + + if (g_strcmp0 (parent_uid_a, "local-stub") == 0) + return -1; + + if (g_strcmp0 (parent_uid_b, "local-stub") == 0) + return 1; + + registry = e_source_config_get_registry (config); + + parent_a = e_source_registry_ref_source (registry, parent_uid_a); + parent_b = e_source_registry_ref_source (registry, parent_uid_b); + + g_return_val_if_fail (parent_a != NULL, 1); + g_return_val_if_fail (parent_b != NULL, -1); + + result = e_source_compare_by_display_name (parent_a, parent_b); + + g_object_unref (parent_a); + g_object_unref (parent_b); + + return result; +} + +static void +source_config_add_candidate (ESourceConfig *config, + ESource *scratch_source, + ESourceConfigBackend *backend) +{ + Candidate *candidate; + GtkBox *backend_box; + GtkLabel *type_label; + GtkComboBoxText *type_combo; + ESource *parent_source; + ESourceRegistry *registry; + const gchar *display_name; + const gchar *parent_uid; + gulong handler_id; + + backend_box = GTK_BOX (config->priv->backend_box); + type_label = GTK_LABEL (config->priv->type_label); + type_combo = GTK_COMBO_BOX_TEXT (config->priv->type_combo); + + registry = e_source_config_get_registry (config); + parent_uid = e_source_get_parent (scratch_source); + parent_source = e_source_registry_ref_source (registry, parent_uid); + g_return_if_fail (parent_source != NULL); + + candidate = g_slice_new (Candidate); + candidate->backend = g_object_ref (backend); + candidate->scratch_source = g_object_ref (scratch_source); + + /* Do not show the page here. */ + candidate->page = g_object_ref_sink (gtk_vbox_new (FALSE, 6)); + gtk_box_pack_start (backend_box, candidate->page, FALSE, FALSE, 0); + + g_ptr_array_add (config->priv->candidates, candidate); + + display_name = e_source_get_display_name (parent_source); + gtk_combo_box_text_append_text (type_combo, display_name); + gtk_label_set_text (type_label, display_name); + + /* Make sure the combo box has a valid active item before + * adding widgets. Otherwise we'll get run-time warnings + * as property bindings are set up. */ + if (gtk_combo_box_get_active (GTK_COMBO_BOX (type_combo)) == -1) + gtk_combo_box_set_active (GTK_COMBO_BOX (type_combo), 0); + + /* Bind the standard widgets to the new scratch source. */ + g_signal_emit ( + config, signals[INIT_CANDIDATE], 0, + candidate->scratch_source); + + /* Insert any backend-specific widgets. */ + e_source_config_backend_insert_widgets ( + candidate->backend, candidate->scratch_source); + + handler_id = g_signal_connect_swapped ( + candidate->scratch_source, "changed", + G_CALLBACK (e_source_config_check_complete), config); + + candidate->changed_handler_id = handler_id; + + /* Trigger the "changed" handler we just connected to set the + * initial "complete" state based on the widgets we just added. */ + e_source_changed (candidate->scratch_source); + + g_object_unref (parent_source); +} + +static void +source_config_free_candidate (Candidate *candidate) +{ + g_signal_handler_disconnect ( + candidate->scratch_source, + candidate->changed_handler_id); + + g_object_unref (candidate->page); + g_object_unref (candidate->scratch_source); + g_object_unref (candidate->backend); + + g_slice_free (Candidate, candidate); +} + +static Candidate * +source_config_get_active_candidate (ESourceConfig *config) +{ + GtkComboBox *type_combo; + gint index; + + type_combo = GTK_COMBO_BOX (config->priv->type_combo); + index = gtk_combo_box_get_active (type_combo); + g_return_val_if_fail (index >= 0, NULL); + + return g_ptr_array_index (config->priv->candidates, index); +} + +static void +source_config_type_combo_changed_cb (GtkComboBox *type_combo, + ESourceConfig *config) +{ + Candidate *candidate; + GPtrArray *array; + gint index; + + array = config->priv->candidates; + + for (index = 0; index < array->len; index++) { + candidate = g_ptr_array_index (array, index); + gtk_widget_hide (candidate->page); + } + + index = gtk_combo_box_get_active (type_combo); + if (index == CLAMP (index, 0, array->len)) { + candidate = g_ptr_array_index (array, index); + gtk_widget_show (candidate->page); + } + + e_source_config_resize_window (config); + e_source_config_check_complete (config); +} + +static gboolean +source_config_init_for_adding_source_foreach (gpointer key, + gpointer value, + gpointer user_data) +{ + ESource *scratch_source; + ESourceBackend *extension; + ESourceConfig *config; + ESourceConfigBackend *backend; + ESourceConfigBackendClass *class; + const gchar *extension_name; + + scratch_source = E_SOURCE (key); + backend = E_SOURCE_CONFIG_BACKEND (value); + config = E_SOURCE_CONFIG (user_data); + + /* This may not be the correct backend name for the child of a + * collection. For example, the "yahoo" collection backend uses + * the "caldav" calender backend for calendar children. But the + * ESourceCollectionBackend can override our setting if needed. */ + class = E_SOURCE_CONFIG_BACKEND_GET_CLASS (backend); + extension_name = e_source_config_get_backend_extension_name (config); + extension = e_source_get_extension (scratch_source, extension_name); + e_source_backend_set_backend_name (extension, class->backend_name); + + source_config_add_candidate (config, scratch_source, backend); + + return FALSE; /* don't stop traversal */ +} + +static void +source_config_init_for_adding_source (ESourceConfig *config) +{ + GList *list, *link; + ESourceRegistry *registry; + GTree *scratch_source_tree; + + /* Candidates are drawn from two sources: + * + * ESourceConfigBackend classes that specify a fixed parent UID, + * meaning there exists one only possible parent source for any + * scratch source created by the backend. The fixed parent UID + * should be a built-in "stub" placeholder ("local-stub", etc). + * + * -and- + * + * Collection sources. We let ESourceConfig subclasses gather + * eligible collection sources to serve as parents for scratch + * sources. A scratch source is matched to a backend based on + * the collection's backend name. The "calendar-enabled" and + * "contacts-enabled" settings also factor into eligibility. + */ + + /* Use a GTree instead of a GHashTable so inserted scratch + * sources automatically sort themselves by their parent's + * display name. */ + scratch_source_tree = g_tree_new_full ( + source_config_compare_sources, config, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) g_object_unref); + + registry = e_source_config_get_registry (config); + + /* First pick out the backends with a fixed parent UID. */ + + list = g_hash_table_get_values (config->priv->backends); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESourceConfigBackend *backend; + ESourceConfigBackendClass *class; + ESource *scratch_source; + ESource *parent_source; + gboolean parent_is_disabled; + + backend = E_SOURCE_CONFIG_BACKEND (link->data); + class = E_SOURCE_CONFIG_BACKEND_GET_CLASS (backend); + + if (class->parent_uid == NULL) + continue; + + /* Verify the fixed parent UID is valid. */ + parent_source = e_source_registry_ref_source ( + registry, class->parent_uid); + if (parent_source == NULL) { + g_warning ( + "%s: %sClass specifies " + "an invalid parent_uid '%s'", + G_STRFUNC, + G_OBJECT_TYPE_NAME (backend), + class->parent_uid); + continue; + } + parent_is_disabled = !e_source_get_enabled (parent_source); + g_object_unref (parent_source); + + /* It's unusual for a fixed parent source to be disabled. + * A user would have to go out of his way to do this, but + * we should honor it regardless. */ + if (parent_is_disabled) + continue; + + /* Some backends don't allow new sources to be created. + * The "contacts" calendar backend is one such example. */ + if (!e_source_config_backend_allow_creation (backend)) + continue; + + scratch_source = e_source_new (NULL, NULL, NULL); + g_return_if_fail (scratch_source != NULL); + + e_source_set_parent (scratch_source, class->parent_uid); + + g_tree_insert ( + scratch_source_tree, + g_object_ref (scratch_source), + g_object_ref (backend)); + + g_object_unref (scratch_source); + } + + g_list_free (list); + + /* Next gather eligible collection sources to serve as parents. */ + + list = e_source_config_list_eligible_collections (config); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESource *parent_source; + ESource *scratch_source; + ESourceBackend *extension; + ESourceConfigBackend *backend = NULL; + const gchar *backend_name; + const gchar *parent_uid; + + parent_source = E_SOURCE (link->data); + parent_uid = e_source_get_uid (parent_source); + + extension = e_source_get_extension ( + parent_source, E_SOURCE_EXTENSION_COLLECTION); + backend_name = e_source_backend_get_backend_name (extension); + + if (backend_name != NULL) + backend = g_hash_table_lookup ( + config->priv->backends, backend_name); + + if (backend == NULL) + continue; + + /* Some backends disallow creating certain types of + * resources. For example, the Exchange Web Services + * backend disallows creating new memo lists. */ + if (!e_source_config_backend_allow_creation (backend)) + continue; + + scratch_source = e_source_new (NULL, NULL, NULL); + g_return_if_fail (scratch_source != NULL); + + e_source_set_parent (scratch_source, parent_uid); + + g_tree_insert ( + scratch_source_tree, + g_object_ref (scratch_source), + g_object_ref (backend)); + + g_object_unref (scratch_source); + } + + g_list_free_full (list, (GDestroyNotify) g_object_unref); + + /* XXX GTree doesn't get as much love as GHashTable. + * It's missing an equivalent to GHashTableIter. */ + g_tree_foreach ( + scratch_source_tree, + source_config_init_for_adding_source_foreach, config); + + g_tree_unref (scratch_source_tree); +} + +static void +source_config_init_for_editing_source (ESourceConfig *config) +{ + ESource *original_source; + ESource *scratch_source; + ESourceBackend *extension; + ESourceConfigBackend *backend; + GDBusObject *dbus_object; + const gchar *backend_name; + const gchar *extension_name; + + original_source = e_source_config_get_original_source (config); + g_return_if_fail (original_source != NULL); + + extension_name = e_source_config_get_backend_extension_name (config); + extension = e_source_get_extension (original_source, extension_name); + backend_name = e_source_backend_get_backend_name (extension); + g_return_if_fail (backend_name != NULL); + + backend = g_hash_table_lookup (config->priv->backends, backend_name); + g_return_if_fail (backend != NULL); + + dbus_object = e_source_ref_dbus_object (original_source); + g_return_if_fail (dbus_object != NULL); + + scratch_source = e_source_new (dbus_object, NULL, NULL); + g_return_if_fail (scratch_source != NULL); + + source_config_add_candidate (config, scratch_source, backend); + + g_object_unref (scratch_source); + g_object_unref (dbus_object); +} + +static void +source_config_set_original_source (ESourceConfig *config, + ESource *original_source) +{ + g_return_if_fail (config->priv->original_source == NULL); + + if (original_source != NULL) + g_object_ref (original_source); + + config->priv->original_source = original_source; +} + +static void +source_config_set_registry (ESourceConfig *config, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (config->priv->registry == NULL); + + config->priv->registry = g_object_ref (registry); +} + +static void +source_config_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ORIGINAL_SOURCE: + source_config_set_original_source ( + E_SOURCE_CONFIG (object), + g_value_get_object (value)); + return; + + case PROP_REGISTRY: + source_config_set_registry ( + E_SOURCE_CONFIG (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_config_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_COLLECTION_SOURCE: + g_value_set_object ( + value, + e_source_config_get_collection_source ( + E_SOURCE_CONFIG (object))); + return; + + case PROP_COMPLETE: + g_value_set_boolean ( + value, + e_source_config_check_complete ( + E_SOURCE_CONFIG (object))); + return; + + case PROP_ORIGINAL_SOURCE: + g_value_set_object ( + value, + e_source_config_get_original_source ( + E_SOURCE_CONFIG (object))); + return; + + case PROP_REGISTRY: + g_value_set_object ( + value, + e_source_config_get_registry ( + E_SOURCE_CONFIG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_config_dispose (GObject *object) +{ + ESourceConfigPrivate *priv; + + priv = E_SOURCE_CONFIG_GET_PRIVATE (object); + + if (priv->original_source != NULL) { + g_object_unref (priv->original_source); + priv->original_source = NULL; + } + + if (priv->collection_source != NULL) { + g_object_unref (priv->collection_source); + priv->collection_source = NULL; + } + + if (priv->registry != NULL) { + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->type_label != NULL) { + g_object_unref (priv->type_label); + priv->type_label = NULL; + } + + if (priv->type_combo != NULL) { + g_object_unref (priv->type_combo); + priv->type_combo = NULL; + } + + if (priv->name_label != NULL) { + g_object_unref (priv->name_label); + priv->name_label = NULL; + } + + if (priv->name_entry != NULL) { + g_object_unref (priv->name_entry); + priv->name_entry = NULL; + } + + if (priv->backend_box != NULL) { + g_object_unref (priv->backend_box); + priv->backend_box = NULL; + } + + if (priv->size_group != NULL) { + g_object_unref (priv->size_group); + priv->size_group = NULL; + } + + g_hash_table_remove_all (priv->backends); + g_ptr_array_set_size (priv->candidates, 0); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_source_config_parent_class)->dispose (object); +} + +static void +source_config_finalize (GObject *object) +{ + ESourceConfigPrivate *priv; + + priv = E_SOURCE_CONFIG_GET_PRIVATE (object); + + g_hash_table_destroy (priv->backends); + g_ptr_array_free (priv->candidates, TRUE); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_source_config_parent_class)->finalize (object); +} + +static void +source_config_constructed (GObject *object) +{ + ESourceConfig *config; + ESourceRegistry *registry; + ESource *original_source; + ESource *collection_source = NULL; + + config = E_SOURCE_CONFIG (object); + registry = e_source_config_get_registry (config); + original_source = e_source_config_get_original_source (config); + + /* If we have an original source, see if it's part + * of a collection and note the collection source. */ + if (original_source != NULL) { + const gchar *extension_name; + + extension_name = E_SOURCE_EXTENSION_COLLECTION; + collection_source = e_source_registry_find_extension ( + registry, original_source, extension_name); + config->priv->collection_source = collection_source; + } + + if (original_source != NULL) + e_source_config_insert_widget ( + config, NULL, _("Type:"), + config->priv->type_label); + else + e_source_config_insert_widget ( + config, NULL, _("Type:"), + config->priv->type_combo); + + /* If the original source is part of a collection then we assume + * the display name is server-assigned and not user-assigned, at + * least not assigned through Evolution. */ + if (collection_source != NULL) + e_source_config_insert_widget ( + config, NULL, _("Name:"), + config->priv->name_label); + else + e_source_config_insert_widget ( + config, NULL, _("Name:"), + config->priv->name_entry); + + source_config_init_backends (config); +} + +static void +source_config_realize (GtkWidget *widget) +{ + ESourceConfig *config; + ESource *original_source; + + /* Chain up to parent's realize() method. */ + GTK_WIDGET_CLASS (e_source_config_parent_class)->realize (widget); + + /* Do this after constructed() so subclasses can fully + * initialize themselves before we add candidates. */ + + config = E_SOURCE_CONFIG (widget); + original_source = e_source_config_get_original_source (config); + + if (original_source == NULL) + source_config_init_for_adding_source (config); + else + source_config_init_for_editing_source (config); +} + +static GList * +source_config_list_eligible_collections (ESourceConfig *config) +{ + ESourceRegistry *registry; + GQueue trash = G_QUEUE_INIT; + GList *list, *link; + const gchar *extension_name; + + extension_name = E_SOURCE_EXTENSION_COLLECTION; + registry = e_source_config_get_registry (config); + + list = e_source_registry_list_sources (registry, extension_name); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESource *source = E_SOURCE (link->data); + gboolean elligible; + + elligible = + e_source_get_enabled (source) && + e_source_get_remote_creatable (source); + + if (!elligible) + g_queue_push_tail (&trash, link); + } + + /* Remove ineligible collections from the list. */ + while ((link = g_queue_pop_head (&trash)) != NULL) { + g_object_unref (link->data); + list = g_list_delete_link (list, link); + } + + return list; +} + +static void +source_config_init_candidate (ESourceConfig *config, + ESource *scratch_source) +{ + g_object_bind_property ( + scratch_source, "display-name", + config->priv->name_label, "label", + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + scratch_source, "display-name", + config->priv->name_entry, "text", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); +} + +static gboolean +source_config_check_complete (ESourceConfig *config, + ESource *scratch_source) +{ + GtkEntry *name_entry; + GtkComboBox *type_combo; + const gchar *text; + + /* Make sure the Type: combo box has a valid item. */ + type_combo = GTK_COMBO_BOX (config->priv->type_combo); + if (gtk_combo_box_get_active (type_combo) < 0) + return FALSE; + + /* Make sure the Name: entry field is not empty. */ + name_entry = GTK_ENTRY (config->priv->name_entry); + text = gtk_entry_get_text (name_entry); + if (text == NULL || *text == '\0') + return FALSE; + + return TRUE; +} + +static void +source_config_commit_changes (ESourceConfig *config, + ESource *scratch_source) +{ + /* Placeholder so subclasses can safely chain up. */ +} + +static void +source_config_resize_window (ESourceConfig *config) +{ + GtkWidget *toplevel; + + /* Expand or shrink our parent window vertically to accommodate + * the newly selected backend's options. Some backends have tons + * of options, some have few. This avoids the case where you + * select a backend with tons of options and then a backend with + * few options and wind up with lots of unused vertical space. */ + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (config)); + + if (GTK_IS_WINDOW (toplevel)) { + GtkWindow *window = GTK_WINDOW (toplevel); + GtkAllocation allocation; + + gtk_widget_get_allocation (toplevel, &allocation); + gtk_window_resize (window, allocation.width, 1); + } +} + +static gboolean +source_config_check_complete_accumulator (GSignalInvocationHint *ihint, + GValue *return_accu, + const GValue *handler_return, + gpointer unused) +{ + gboolean v_boolean; + + /* Abort emission if a handler returns FALSE. */ + v_boolean = g_value_get_boolean (handler_return); + g_value_set_boolean (return_accu, v_boolean); + + return v_boolean; +} + +static void +e_source_config_class_init (ESourceConfigClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (ESourceConfigPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = source_config_set_property; + object_class->get_property = source_config_get_property; + object_class->dispose = source_config_dispose; + object_class->finalize = source_config_finalize; + object_class->constructed = source_config_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->realize = source_config_realize; + + class->list_eligible_collections = + source_config_list_eligible_collections; + class->init_candidate = source_config_init_candidate; + class->check_complete = source_config_check_complete; + class->commit_changes = source_config_commit_changes; + class->resize_window = source_config_resize_window; + + g_object_class_install_property ( + object_class, + PROP_COLLECTION_SOURCE, + g_param_spec_object ( + "collection-source", + "Collection Source", + "The collection ESource to which " + "the ESource being edited belongs", + E_TYPE_SOURCE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_COMPLETE, + g_param_spec_boolean ( + "complete", + "Complete", + "Are the required fields complete?", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_ORIGINAL_SOURCE, + g_param_spec_object ( + "original-source", + "Original Source", + "The original ESource", + E_TYPE_SOURCE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + "Registry of ESources", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + signals[CHECK_COMPLETE] = g_signal_new ( + "check-complete", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESourceConfigClass, check_complete), + source_config_check_complete_accumulator, NULL, + e_marshal_BOOLEAN__OBJECT, + G_TYPE_BOOLEAN, 1, + E_TYPE_SOURCE); + + signals[COMMIT_CHANGES] = g_signal_new ( + "commit-changes", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESourceConfigClass, commit_changes), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + E_TYPE_SOURCE); + + signals[INIT_CANDIDATE] = g_signal_new ( + "init-candidate", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESourceConfigClass, init_candidate), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + E_TYPE_SOURCE); + + signals[RESIZE_WINDOW] = g_signal_new ( + "resize-window", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESourceConfigClass, resize_window), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +e_source_config_init (ESourceConfig *config) +{ + GPtrArray *candidates; + GtkSizeGroup *size_group; + PangoAttribute *attr; + PangoAttrList *attr_list; + GtkWidget *widget; + + /* The candidates array holds scratch ESources, one for each + * item in the "type" combo box. Scratch ESources are never + * added to the registry, so backend extensions can make any + * changes they want to them. Whichever scratch ESource is + * "active" (selected in the "type" combo box) when the user + * clicks OK wins and is written to disk. The others are + * discarded. */ + candidates = g_ptr_array_new_with_free_func ( + (GDestroyNotify) source_config_free_candidate); + + /* The size group is used for caption labels. */ + size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + gtk_box_set_spacing (GTK_BOX (config), 6); + + gtk_orientable_set_orientation ( + GTK_ORIENTABLE (config), GTK_ORIENTATION_VERTICAL); + + config->priv = E_SOURCE_CONFIG_GET_PRIVATE (config); + config->priv->candidates = candidates; + config->priv->size_group = size_group; + + attr_list = pango_attr_list_new (); + + attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD); + pango_attr_list_insert (attr_list, attr); + + /* Either the source type combo box or the label is shown, + * never both. But we create both widgets and keep them + * both up-to-date because it makes the logic simpler. */ + + widget = gtk_label_new (NULL); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_label_set_attributes (GTK_LABEL (widget), attr_list); + config->priv->type_label = g_object_ref_sink (widget); + gtk_widget_show (widget); + + widget = gtk_combo_box_text_new (); + config->priv->type_combo = g_object_ref_sink (widget); + gtk_widget_show (widget); + + /* Similarly for the display name. Either the text entry + * or the label is shown, depending on whether the source + * is a collection member (new sources never are). */ + + widget = gtk_label_new (NULL); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_label_set_attributes (GTK_LABEL (widget), attr_list); + config->priv->name_label = g_object_ref_sink (widget); + gtk_widget_show (widget); + + widget = gtk_entry_new (); + gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE); + config->priv->name_entry = g_object_ref_sink (widget); + gtk_widget_show (widget); + + /* The backend box holds backend-specific options. Each backend + * gets a child widget. Only one child widget is visible at once. */ + widget = gtk_vbox_new (FALSE, 12); + gtk_box_pack_end (GTK_BOX (config), widget, TRUE, TRUE, 0); + config->priv->backend_box = g_object_ref (widget); + gtk_widget_show (widget); + + pango_attr_list_unref (attr_list); + + g_signal_connect ( + config->priv->type_combo, "changed", + G_CALLBACK (source_config_type_combo_changed_cb), config); +} + +GtkWidget * +e_source_config_new (ESourceRegistry *registry, + ESource *original_source) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + if (original_source != NULL) + g_return_val_if_fail (E_IS_SOURCE (original_source), NULL); + + return g_object_new ( + E_TYPE_SOURCE_CONFIG, "registry", registry, + "original-source", original_source, NULL); +} + +void +e_source_config_insert_widget (ESourceConfig *config, + ESource *scratch_source, + const gchar *caption, + GtkWidget *widget) +{ + GtkWidget *hbox; + GtkWidget *vbox; + GtkWidget *label; + + g_return_if_fail (E_IS_SOURCE_CONFIG (config)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + if (scratch_source == NULL) + vbox = GTK_WIDGET (config); + else + vbox = e_source_config_get_page (config, scratch_source); + + hbox = gtk_hbox_new (FALSE, 12); + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, TRUE, 0); + + g_object_bind_property ( + widget, "visible", + hbox, "visible", + G_BINDING_SYNC_CREATE); + + label = gtk_label_new (caption); + gtk_misc_set_alignment (GTK_MISC (label), 1.0, 0.5); + gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, TRUE, 0); + gtk_size_group_add_widget (config->priv->size_group, label); + gtk_widget_show (label); + + gtk_box_pack_start (GTK_BOX (hbox), widget, TRUE, TRUE, 0); +} + +GtkWidget * +e_source_config_get_page (ESourceConfig *config, + ESource *scratch_source) +{ + Candidate *candidate; + GtkWidget *page = NULL; + GPtrArray *array; + gint index; + + g_return_val_if_fail (E_IS_SOURCE_CONFIG (config), NULL); + g_return_val_if_fail (E_IS_SOURCE (scratch_source), NULL); + + array = config->priv->candidates; + + for (index = 0; page == NULL && index < array->len; index++) { + candidate = g_ptr_array_index (array, index); + if (e_source_equal (scratch_source, candidate->scratch_source)) + page = candidate->page; + } + + g_return_val_if_fail (GTK_IS_BOX (page), NULL); + + return page; +} + +const gchar * +e_source_config_get_backend_extension_name (ESourceConfig *config) +{ + ESourceConfigClass *class; + + g_return_val_if_fail (E_IS_SOURCE_CONFIG (config), NULL); + + class = E_SOURCE_CONFIG_GET_CLASS (config); + g_return_val_if_fail (class->get_backend_extension_name != NULL, NULL); + + return class->get_backend_extension_name (config); +} + +GList * +e_source_config_list_eligible_collections (ESourceConfig *config) +{ + ESourceConfigClass *class; + + g_return_val_if_fail (E_IS_SOURCE_CONFIG (config), NULL); + + class = E_SOURCE_CONFIG_GET_CLASS (config); + g_return_val_if_fail (class->list_eligible_collections != NULL, NULL); + + return class->list_eligible_collections (config); +} + +gboolean +e_source_config_check_complete (ESourceConfig *config) +{ + Candidate *candidate; + gboolean complete; + + g_return_val_if_fail (E_IS_SOURCE_CONFIG (config), FALSE); + + candidate = source_config_get_active_candidate (config); + g_return_val_if_fail (candidate != NULL, FALSE); + + g_signal_emit ( + config, signals[CHECK_COMPLETE], 0, + candidate->scratch_source, &complete); + + complete &= e_source_config_backend_check_complete ( + candidate->backend, candidate->scratch_source); + + /* XXX Emitting "notify::complete" may cause this function + * to be called repeatedly by signal handlers. The IF + * check below should break any recursive cycles. Not + * very efficient but I think we can live with it. */ + + if (complete != config->priv->complete) { + config->priv->complete = complete; + g_object_notify (G_OBJECT (config), "complete"); + } + + return complete; +} + +ESource * +e_source_config_get_original_source (ESourceConfig *config) +{ + g_return_val_if_fail (E_IS_SOURCE_CONFIG (config), NULL); + + return config->priv->original_source; +} + +ESource * +e_source_config_get_collection_source (ESourceConfig *config) +{ + g_return_val_if_fail (E_IS_SOURCE_CONFIG (config), NULL); + + return config->priv->collection_source; +} + +ESourceRegistry * +e_source_config_get_registry (ESourceConfig *config) +{ + g_return_val_if_fail (E_IS_SOURCE_CONFIG (config), NULL); + + return config->priv->registry; +} + +void +e_source_config_resize_window (ESourceConfig *config) +{ + g_return_if_fail (E_IS_SOURCE_CONFIG (config)); + + g_signal_emit (config, signals[RESIZE_WINDOW], 0); +} + +/* Helper for e_source_config_commit() */ +static void +source_config_commit_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + GError *error = NULL; + + simple = G_SIMPLE_ASYNC_RESULT (user_data); + + e_source_registry_commit_source_finish ( + E_SOURCE_REGISTRY (object), result, &error); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); + + g_simple_async_result_complete (simple); + g_object_unref (simple); +} + +void +e_source_config_commit (ESourceConfig *config, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + ESourceRegistry *registry; + Candidate *candidate; + + g_return_if_fail (E_IS_SOURCE_CONFIG (config)); + + registry = e_source_config_get_registry (config); + + candidate = source_config_get_active_candidate (config); + g_return_if_fail (candidate != NULL); + + e_source_config_backend_commit_changes ( + candidate->backend, candidate->scratch_source); + + g_signal_emit ( + config, signals[COMMIT_CHANGES], 0, + candidate->scratch_source); + + simple = g_simple_async_result_new ( + G_OBJECT (config), callback, + user_data, e_source_config_commit); + + e_source_registry_commit_source ( + registry, candidate->scratch_source, + cancellable, source_config_commit_cb, simple); +} + +gboolean +e_source_config_commit_finish (ESourceConfig *config, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (config), + e_source_config_commit), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + + /* Assume success unless a GError is set. */ + return !g_simple_async_result_propagate_error (simple, error); +} + +void +e_source_config_add_refresh_interval (ESourceConfig *config, + ESource *scratch_source) +{ + GtkWidget *widget; + GtkWidget *container; + ESourceExtension *extension; + const gchar *extension_name; + + g_return_if_fail (E_IS_SOURCE_CONFIG (config)); + g_return_if_fail (E_IS_SOURCE (scratch_source)); + + extension_name = E_SOURCE_EXTENSION_REFRESH; + extension = e_source_get_extension (scratch_source, extension_name); + + widget = gtk_alignment_new (0.0, 0.5, 0.0, 0.0); + e_source_config_insert_widget (config, scratch_source, NULL, widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_hbox_new (FALSE, 6); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + container = widget; + + /* Translators: This is the first of a sequence of widgets: + * "Refresh every [NUMERIC_ENTRY] [TIME_UNITS_COMBO]" */ + widget = gtk_label_new (_("Refresh every")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = e_interval_chooser_new (); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + g_object_bind_property ( + extension, "interval-minutes", + widget, "interval-minutes", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); +} + +void +e_source_config_add_secure_connection (ESourceConfig *config, + ESource *scratch_source) +{ + GtkWidget *widget; + ESourceExtension *extension; + const gchar *extension_name; + const gchar *label; + + g_return_if_fail (E_IS_SOURCE_CONFIG (config)); + g_return_if_fail (E_IS_SOURCE (scratch_source)); + + extension_name = E_SOURCE_EXTENSION_SECURITY; + extension = e_source_get_extension (scratch_source, extension_name); + + label = _("Use a secure connection"); + widget = gtk_check_button_new_with_label (label); + e_source_config_insert_widget (config, scratch_source, NULL, widget); + gtk_widget_show (widget); + + g_object_bind_property ( + extension, "secure", + widget, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); +} + +static gboolean +secure_to_port_cb (GBinding *binding, + const GValue *source_value, + GValue *target_value, + gpointer user_data) +{ + GObject *authentication_extension; + guint16 port; + + authentication_extension = g_binding_get_target (binding); + g_object_get (authentication_extension, "port", &port, NULL); + + if (port == 80 || port == 443 || port == 0) + port = g_value_get_boolean (source_value) ? 443 : 80; + + g_value_set_uint (target_value, port); + + return TRUE; +} + +void +e_source_config_add_secure_connection_for_webdav (ESourceConfig *config, + ESource *scratch_source) +{ + GtkWidget *widget1; + GtkWidget *widget2; + ESourceExtension *extension; + ESourceAuthentication *authentication_extension; + const gchar *extension_name; + const gchar *label; + + g_return_if_fail (E_IS_SOURCE_CONFIG (config)); + g_return_if_fail (E_IS_SOURCE (scratch_source)); + + extension_name = E_SOURCE_EXTENSION_SECURITY; + extension = e_source_get_extension (scratch_source, extension_name); + + label = _("Use a secure connection"); + widget1 = gtk_check_button_new_with_label (label); + e_source_config_insert_widget (config, scratch_source, NULL, widget1); + gtk_widget_show (widget1); + + g_object_bind_property ( + extension, "secure", + widget1, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + extension_name = E_SOURCE_EXTENSION_AUTHENTICATION; + authentication_extension = e_source_get_extension (scratch_source, extension_name); + + g_object_bind_property_full ( + extension, "secure", + authentication_extension, "port", + G_BINDING_DEFAULT, + secure_to_port_cb, + NULL, NULL, NULL); + + extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND; + extension = e_source_get_extension (scratch_source, extension_name); + + label = _("Ignore invalid SSL certificate"); + widget2 = gtk_check_button_new_with_label (label); + gtk_widget_set_margin_left (widget2, 24); + e_source_config_insert_widget (config, scratch_source, NULL, widget2); + gtk_widget_show (widget2); + + g_object_bind_property ( + widget1, "active", + widget2, "sensitive", + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + extension, "ignore-invalid-cert", + widget2, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); +} + +void +e_source_config_add_user_entry (ESourceConfig *config, + ESource *scratch_source) +{ + GtkWidget *widget; + ESource *original_source; + ESourceExtension *extension; + const gchar *extension_name; + + g_return_if_fail (E_IS_SOURCE_CONFIG (config)); + g_return_if_fail (E_IS_SOURCE (scratch_source)); + + extension_name = E_SOURCE_EXTENSION_AUTHENTICATION; + extension = e_source_get_extension (scratch_source, extension_name); + + original_source = e_source_config_get_original_source (config); + + widget = gtk_entry_new (); + e_source_config_insert_widget ( + config, scratch_source, _("User"), widget); + gtk_widget_show (widget); + + g_object_bind_property ( + extension, "user", + widget, "text", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + /* If this is a new data source, initialize the + * GtkEntry to the user name of the current user. */ + if (original_source == NULL) + gtk_entry_set_text (GTK_ENTRY (widget), g_get_user_name ()); +} + diff --git a/e-util/e-source-config.h b/e-util/e-source-config.h new file mode 100644 index 0000000000..3868c0309b --- /dev/null +++ b/e-util/e-source-config.h @@ -0,0 +1,120 @@ +/* + * e-source-config.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SOURCE_CONFIG_H +#define E_SOURCE_CONFIG_H + +#include <gtk/gtk.h> +#include <libedataserver/libedataserver.h> + +/* Standard GObject macros */ +#define E_TYPE_SOURCE_CONFIG \ + (e_source_config_get_type ()) +#define E_SOURCE_CONFIG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SOURCE_CONFIG, ESourceConfig)) +#define E_SOURCE_CONFIG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SOURCE_CONFIG, ESourceConfigClass)) +#define E_IS_SOURCE_CONFIG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SOURCE_CONFIG)) +#define E_IS_SOURCE_CONFIG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SOURCE_CONFIG)) +#define E_SOURCE_CONFIG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SOURCE_CONFIG, ESourceConfigClass)) + +G_BEGIN_DECLS + +typedef struct _ESourceConfig ESourceConfig; +typedef struct _ESourceConfigClass ESourceConfigClass; +typedef struct _ESourceConfigPrivate ESourceConfigPrivate; + +struct _ESourceConfig { + GtkBox parent; + ESourceConfigPrivate *priv; +}; + +struct _ESourceConfigClass { + GtkBoxClass parent_class; + + /* Methods */ + const gchar * (*get_backend_extension_name) + (ESourceConfig *config); + GList * (*list_eligible_collections) + (ESourceConfig *config); + + /* Signals */ + void (*init_candidate) (ESourceConfig *config, + ESource *scratch_source); + gboolean (*check_complete) (ESourceConfig *config, + ESource *scratch_source); + void (*commit_changes) (ESourceConfig *config, + ESource *scratch_source); + void (*resize_window) (ESourceConfig *config); +}; + +GType e_source_config_get_type (void) G_GNUC_CONST; +GtkWidget * e_source_config_new (ESourceRegistry *registry, + ESource *original_source); +void e_source_config_insert_widget (ESourceConfig *config, + ESource *scratch_source, + const gchar *caption, + GtkWidget *widget); +GtkWidget * e_source_config_get_page (ESourceConfig *config, + ESource *scratch_source); +const gchar * e_source_config_get_backend_extension_name + (ESourceConfig *config); +GList * e_source_config_list_eligible_collections + (ESourceConfig *config); +gboolean e_source_config_check_complete (ESourceConfig *config); +ESource * e_source_config_get_original_source + (ESourceConfig *config); +ESource * e_source_config_get_collection_source + (ESourceConfig *config); +ESourceRegistry * + e_source_config_get_registry (ESourceConfig *config); +void e_source_config_resize_window (ESourceConfig *config); +void e_source_config_commit (ESourceConfig *config, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_source_config_commit_finish (ESourceConfig *config, + GAsyncResult *result, + GError **error); + +/* Convenience functions for common settings. */ +void e_source_config_add_refresh_interval + (ESourceConfig *config, + ESource *scratch_source); +void e_source_config_add_secure_connection + (ESourceConfig *config, + ESource *scratch_source); +void e_source_config_add_secure_connection_for_webdav + (ESourceConfig *config, + ESource *scratch_source); +void e_source_config_add_user_entry (ESourceConfig *config, + ESource *scratch_source); + +#endif /* E_SOURCE_CONFIG_H */ diff --git a/e-util/e-source-selector-dialog.c b/e-util/e-source-selector-dialog.c new file mode 100644 index 0000000000..68e29fd13c --- /dev/null +++ b/e-util/e-source-selector-dialog.c @@ -0,0 +1,453 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-source-selector-dialog.c + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Rodrigo Moya <rodrigo@novell.com> + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <glib/gi18n-lib.h> +#include "e-source-selector.h" +#include "e-source-selector-dialog.h" + +#define E_SOURCE_SELECTOR_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SOURCE_SELECTOR_DIALOG, ESourceSelectorDialogPrivate)) + +struct _ESourceSelectorDialogPrivate { + GtkWidget *selector; + ESourceRegistry *registry; + ESource *selected_source; + gchar *extension_name; +}; + +enum { + PROP_0, + PROP_EXTENSION_NAME, + PROP_REGISTRY, + PROP_SELECTOR +}; + +G_DEFINE_TYPE ( + ESourceSelectorDialog, + e_source_selector_dialog, + GTK_TYPE_DIALOG) + +static void +source_selector_dialog_row_activated_cb (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column, + GtkWidget *dialog) +{ + gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); +} + +static void +primary_selection_changed_cb (ESourceSelector *selector, + ESourceSelectorDialog *dialog) +{ + ESourceSelectorDialogPrivate *priv = dialog->priv; + + if (priv->selected_source != NULL) + g_object_unref (priv->selected_source); + priv->selected_source = + e_source_selector_ref_primary_selection (selector); + + /* FIXME Add an API for "except-source" or to + * get the ESourceSelector from outside. */ + if (priv->selected_source != NULL) { + ESource *except_source; + + except_source = g_object_get_data ( + G_OBJECT (dialog), "except-source"); + + if (except_source != NULL) + if (e_source_equal (except_source, priv->selected_source)) { + g_object_unref (priv->selected_source); + priv->selected_source = NULL; + } + } + + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (dialog), GTK_RESPONSE_OK, + (priv->selected_source != NULL)); +} + +static void +source_selector_dialog_set_extension_name (ESourceSelectorDialog *dialog, + const gchar *extension_name) +{ + g_return_if_fail (extension_name != NULL); + g_return_if_fail (dialog->priv->extension_name == NULL); + + dialog->priv->extension_name = g_strdup (extension_name); +} + +static void +source_selector_dialog_set_registry (ESourceSelectorDialog *dialog, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (dialog->priv->registry == NULL); + + dialog->priv->registry = g_object_ref (registry); +} + +static void +source_selector_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_EXTENSION_NAME: + source_selector_dialog_set_extension_name ( + E_SOURCE_SELECTOR_DIALOG (object), + g_value_get_string (value)); + return; + + case PROP_REGISTRY: + source_selector_dialog_set_registry ( + E_SOURCE_SELECTOR_DIALOG (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_selector_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_EXTENSION_NAME: + g_value_set_string ( + value, + e_source_selector_dialog_get_extension_name ( + E_SOURCE_SELECTOR_DIALOG (object))); + return; + + case PROP_REGISTRY: + g_value_set_object ( + value, + e_source_selector_dialog_get_registry ( + E_SOURCE_SELECTOR_DIALOG (object))); + return; + + case PROP_SELECTOR: + g_value_set_object ( + value, + e_source_selector_dialog_get_selector ( + E_SOURCE_SELECTOR_DIALOG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_selector_dialog_dispose (GObject *object) +{ + ESourceSelectorDialogPrivate *priv; + + priv = E_SOURCE_SELECTOR_DIALOG_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_object_unref (priv->registry); + priv->registry = NULL; + } + + if (priv->selected_source != NULL) { + g_object_unref (priv->selected_source); + priv->selected_source = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_source_selector_dialog_parent_class)->dispose (object); +} + +static void +source_selector_dialog_finalize (GObject *object) +{ + ESourceSelectorDialogPrivate *priv; + + priv = E_SOURCE_SELECTOR_DIALOG_GET_PRIVATE (object); + + g_free (priv->extension_name); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_source_selector_dialog_parent_class)->finalize (object); +} + +static void +source_selector_dialog_constructed (GObject *object) +{ + ESourceSelectorDialog *dialog; + GtkWidget *label, *hgrid; + GtkWidget *container; + GtkWidget *widget; + gchar *label_text; + + dialog = E_SOURCE_SELECTOR_DIALOG (object); + + container = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + + widget = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_VERTICAL, + "column-homogeneous", FALSE, + "row-spacing", 12, + NULL); + gtk_container_set_border_width (GTK_CONTAINER (widget), 12); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + label_text = g_strdup_printf ("<b>%s</b>", _("_Destination")); + label = gtk_label_new_with_mnemonic (label_text); + gtk_label_set_use_markup (GTK_LABEL (label), TRUE); + gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5); + gtk_container_add (GTK_CONTAINER (container), label); + gtk_widget_show (label); + g_free (label_text); + + hgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "row-homogeneous", FALSE, + "column-spacing", 12, + "vexpand", TRUE, + "valign", GTK_ALIGN_FILL, + NULL); + gtk_container_add (GTK_CONTAINER (container), hgrid); + gtk_widget_show (hgrid); + + widget = gtk_label_new (""); + gtk_container_add (GTK_CONTAINER (hgrid), widget); + gtk_widget_show (widget); + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_widget_set_hexpand (widget, TRUE); + gtk_widget_set_halign (widget, GTK_ALIGN_FILL); + gtk_widget_set_vexpand (widget, TRUE); + gtk_widget_set_valign (widget, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (hgrid), widget); + gtk_widget_show (widget); + + container = widget; + + widget = e_source_selector_new ( + dialog->priv->registry, + dialog->priv->extension_name); + e_source_selector_set_show_toggles (E_SOURCE_SELECTOR (widget), FALSE); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget); + gtk_container_add (GTK_CONTAINER (container), widget); + dialog->priv->selector = widget; + gtk_widget_show (widget); + + g_signal_connect ( + widget, "row_activated", + G_CALLBACK (source_selector_dialog_row_activated_cb), dialog); + g_signal_connect ( + widget, "primary_selection_changed", + G_CALLBACK (primary_selection_changed_cb), dialog); +} + +static void +e_source_selector_dialog_class_init (ESourceSelectorDialogClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ESourceSelectorDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = source_selector_dialog_set_property; + object_class->get_property = source_selector_dialog_get_property; + object_class->dispose = source_selector_dialog_dispose; + object_class->finalize = source_selector_dialog_finalize; + object_class->constructed = source_selector_dialog_constructed; + + g_object_class_install_property ( + object_class, + PROP_EXTENSION_NAME, + g_param_spec_string ( + "extension-name", + NULL, + NULL, + NULL, + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY)); + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + NULL, + NULL, + E_TYPE_SOURCE_REGISTRY, + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY)); + + g_object_class_install_property ( + object_class, + PROP_SELECTOR, + g_param_spec_object ( + "selector", + NULL, + NULL, + E_TYPE_SOURCE_SELECTOR, + G_PARAM_READABLE)); +} + +static void +e_source_selector_dialog_init (ESourceSelectorDialog *dialog) +{ + GtkWidget *action_area; + GtkWidget *content_area; + + dialog->priv = E_SOURCE_SELECTOR_DIALOG_GET_PRIVATE (dialog); + + action_area = gtk_dialog_get_action_area (GTK_DIALOG (dialog)); + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + + gtk_window_set_title (GTK_WINDOW (dialog), _("Select destination")); + gtk_window_set_default_size (GTK_WINDOW (dialog), 320, 240); + + gtk_widget_ensure_style (GTK_WIDGET (dialog)); + gtk_container_set_border_width (GTK_CONTAINER (content_area), 0); + gtk_container_set_border_width (GTK_CONTAINER (action_area), 12); + + gtk_dialog_add_buttons ( + GTK_DIALOG (dialog), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, NULL); + gtk_dialog_set_default_response ( + GTK_DIALOG (dialog), GTK_RESPONSE_OK); + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (dialog), GTK_RESPONSE_OK, FALSE); +} + +/** + * e_source_selector_dialog_new: + * @parent: a parent window + * @registry: an #ESourceRegistry + * @extension_name: the name of an #ESource extension + * + * Displays a list of sources from @registry having an extension named + * @extension_name in a dialog window. The sources are grouped by backend + * or groupware account, which are described by the parent source. + * + * Returns: a new #ESourceSelectorDialog + **/ +GtkWidget * +e_source_selector_dialog_new (GtkWindow *parent, + ESourceRegistry *registry, + const gchar *extension_name) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + g_return_val_if_fail (extension_name != NULL, NULL); + + return g_object_new ( + E_TYPE_SOURCE_SELECTOR_DIALOG, + "transient-for", parent, + "registry", registry, + "extension-name", extension_name, + NULL); +} + +/** + * e_source_selector_dialog_get_registry: + * @dialog: an #ESourceSelectorDialog + * + * Returns the #ESourceRegistry passed to e_source_selector_dialog_new(). + * + * Returns: the #ESourceRegistry for @dialog + * + * Since: 3.6 + **/ +ESourceRegistry * +e_source_selector_dialog_get_registry (ESourceSelectorDialog *dialog) +{ + g_return_val_if_fail (E_IS_SOURCE_SELECTOR_DIALOG (dialog), NULL); + + return dialog->priv->registry; +} + +/** + * e_source_selector_dialog_get_extension_name: + * @dialog: an #ESourceSelectorDialog + * + * Returns the extension name passed to e_source_selector_dialog_new(). + * + * Returns: the extension name for @dialog + * + * Since: 3.6 + **/ +const gchar * +e_source_selector_dialog_get_extension_name (ESourceSelectorDialog *dialog) +{ + g_return_val_if_fail (E_IS_SOURCE_SELECTOR_DIALOG (dialog), NULL); + + return dialog->priv->extension_name; +} + +/** + * e_source_selector_dialog_get_selector: + * @dialog: an #ESourceSelectorDialog + * + * Returns the #ESourceSelector widget embedded in @dialog. + * + * Returns: the #ESourceSelector widget + * + * Since: 3.6 + **/ +ESourceSelector * +e_source_selector_dialog_get_selector (ESourceSelectorDialog *dialog) +{ + g_return_val_if_fail (E_IS_SOURCE_SELECTOR_DIALOG (dialog), NULL); + + return E_SOURCE_SELECTOR (dialog->priv->selector); +} + +/** + * e_source_selector_dialog_peek_primary_selection: + * @dialog: an #ESourceSelectorDialog + * + * Peek the currently selected source in the given @dialog. + * + * Returns: the selected #ESource + */ +ESource * +e_source_selector_dialog_peek_primary_selection (ESourceSelectorDialog *dialog) +{ + g_return_val_if_fail (E_IS_SOURCE_SELECTOR_DIALOG (dialog), NULL); + + return dialog->priv->selected_source; +} diff --git a/e-util/e-source-selector-dialog.h b/e-util/e-source-selector-dialog.h new file mode 100644 index 0000000000..eae45ba62f --- /dev/null +++ b/e-util/e-source-selector-dialog.h @@ -0,0 +1,85 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-source-selector-dialog.h + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Rodrigo Moya <rodrigo@novell.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SOURCE_SELECTOR_DIALOG_H +#define E_SOURCE_SELECTOR_DIALOG_H + +#include <gtk/gtk.h> +#include <e-util/e-source-selector.h> + +/* Standard GObject macros */ +#define E_TYPE_SOURCE_SELECTOR_DIALOG \ + (e_source_selector_dialog_get_type ()) +#define E_SOURCE_SELECTOR_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SOURCE_SELECTOR_DIALOG, ESourceSelectorDialog)) +#define E_SOURCE_SELECTOR_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SOURCE_SELECTOR_DIALOG, ESourceSelectorDialogClass)) +#define E_IS_SOURCE_SELECTOR_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SOURCE_SELECTOR_DIALOG)) +#define E_IS_SOURCE_SELECTOR_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SOURCE_SELECTOR_DIALOG)) +#define E_SOURCE_SELECTOR_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SOURCE_SELECTOR_DIALOG, ESourceSelectorDialogClass)) + +G_BEGIN_DECLS + +typedef struct _ESourceSelectorDialog ESourceSelectorDialog; +typedef struct _ESourceSelectorDialogClass ESourceSelectorDialogClass; +typedef struct _ESourceSelectorDialogPrivate ESourceSelectorDialogPrivate; + +struct _ESourceSelectorDialog { + GtkDialog parent; + ESourceSelectorDialogPrivate *priv; +}; + +struct _ESourceSelectorDialogClass { + GtkDialogClass parent_class; +}; + +GType e_source_selector_dialog_get_type (void); +GtkWidget * e_source_selector_dialog_new (GtkWindow *parent, + ESourceRegistry *registry, + const gchar *extension_name); +ESourceRegistry * + e_source_selector_dialog_get_registry + (ESourceSelectorDialog *dialog); +const gchar * e_source_selector_dialog_get_extension_name + (ESourceSelectorDialog *dialog); +ESourceSelector * + e_source_selector_dialog_get_selector + (ESourceSelectorDialog *dialog); +ESource * e_source_selector_dialog_peek_primary_selection + (ESourceSelectorDialog *dialog); + +G_END_DECLS + +#endif /* E_SOURCE_SELECTOR_DIALOG_H */ diff --git a/e-util/e-source-selector.c b/e-util/e-source-selector.c new file mode 100644 index 0000000000..4a75ed10e5 --- /dev/null +++ b/e-util/e-source-selector.c @@ -0,0 +1,2082 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-source-selector.c + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Ettore Perazzoli <ettore@ximian.com> + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include "e-cell-renderer-color.h" +#include "e-source-selector.h" + +#define E_SOURCE_SELECTOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SOURCE_SELECTOR, ESourceSelectorPrivate)) + +typedef struct _AsyncContext AsyncContext; + +struct _ESourceSelectorPrivate { + ESourceRegistry *registry; + GHashTable *source_index; + gchar *extension_name; + + GtkTreeRowReference *saved_primary_selection; + + /* ESource -> GSource */ + GHashTable *pending_writes; + GMainContext *main_context; + + gboolean toggled_last; + gboolean select_new; + gboolean show_colors; + gboolean show_toggles; +}; + +struct _AsyncContext { + ESourceSelector *selector; + ESource *source; +}; + +enum { + PROP_0, + PROP_EXTENSION_NAME, + PROP_PRIMARY_SELECTION, + PROP_REGISTRY, + PROP_SHOW_COLORS, + PROP_SHOW_TOGGLES +}; + +enum { + SELECTION_CHANGED, + PRIMARY_SELECTION_CHANGED, + POPUP_EVENT, + DATA_DROPPED, + NUM_SIGNALS +}; + +enum { + COLUMN_NAME, + COLUMN_COLOR, + COLUMN_ACTIVE, + COLUMN_SHOW_COLOR, + COLUMN_SHOW_TOGGLE, + COLUMN_WEIGHT, + COLUMN_SOURCE, + NUM_COLUMNS +}; + +static guint signals[NUM_SIGNALS]; + +G_DEFINE_TYPE (ESourceSelector, e_source_selector, GTK_TYPE_TREE_VIEW) + +/* ESafeToggleRenderer does not emit 'toggled' signal + * on 'activate' when mouse is not over the toggle. */ + +typedef GtkCellRendererToggle ECellRendererSafeToggle; +typedef GtkCellRendererToggleClass ECellRendererSafeToggleClass; + +/* Forward Declarations */ +GType e_cell_renderer_safe_toggle_get_type (void); + +G_DEFINE_TYPE ( + ECellRendererSafeToggle, + e_cell_renderer_safe_toggle, + GTK_TYPE_CELL_RENDERER_TOGGLE) + +static gboolean +safe_toggle_activate (GtkCellRenderer *cell, + GdkEvent *event, + GtkWidget *widget, + const gchar *path, + const GdkRectangle *background_area, + const GdkRectangle *cell_area, + GtkCellRendererState flags) +{ + gboolean point_in_cell_area = TRUE; + + if (event->type == GDK_BUTTON_PRESS && cell_area != NULL) { + cairo_region_t *region; + + region = cairo_region_create_rectangle (cell_area); + point_in_cell_area = cairo_region_contains_point ( + region, event->button.x, event->button.y); + cairo_region_destroy (region); + } + + if (!point_in_cell_area) + return FALSE; + + return GTK_CELL_RENDERER_CLASS ( + e_cell_renderer_safe_toggle_parent_class)->activate ( + cell, event, widget, path, background_area, cell_area, flags); +} + +static void +e_cell_renderer_safe_toggle_class_init (ECellRendererSafeToggleClass *class) +{ + GtkCellRendererClass *cell_renderer_class; + + cell_renderer_class = GTK_CELL_RENDERER_CLASS (class); + cell_renderer_class->activate = safe_toggle_activate; +} + +static void +e_cell_renderer_safe_toggle_init (ECellRendererSafeToggle *obj) +{ +} + +static GtkCellRenderer * +e_cell_renderer_safe_toggle_new (void) +{ + return g_object_new (e_cell_renderer_safe_toggle_get_type (), NULL); +} + +static void +clear_saved_primary_selection (ESourceSelector *selector) +{ + gtk_tree_row_reference_free (selector->priv->saved_primary_selection); + selector->priv->saved_primary_selection = NULL; +} + +static void +async_context_free (AsyncContext *async_context) +{ + if (async_context->selector != NULL) + g_object_unref (async_context->selector); + + if (async_context->source != NULL) + g_object_unref (async_context->source); + + g_slice_free (AsyncContext, async_context); +} + +static void +pending_writes_destroy_source (GSource *source) +{ + g_source_destroy (source); + g_source_unref (source); +} + +static void +source_selector_write_done_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + ESource *source; + ESourceSelector *selector; + GError *error = NULL; + + source = E_SOURCE (source_object); + selector = E_SOURCE_SELECTOR (user_data); + + e_source_write_finish (source, result, &error); + + /* FIXME Display the error in the selector somehow? */ + if (error != NULL) { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_error_free (error); + } + + g_object_unref (selector); +} + +static gboolean +source_selector_write_idle_cb (gpointer user_data) +{ + AsyncContext *async_context = user_data; + GHashTable *pending_writes; + + /* XXX This operation is not cancellable. */ + e_source_write ( + async_context->source, NULL, + source_selector_write_done_cb, + g_object_ref (async_context->selector)); + + pending_writes = async_context->selector->priv->pending_writes; + g_hash_table_remove (pending_writes, async_context->source); + + return FALSE; +} + +static void +source_selector_cancel_write (ESourceSelector *selector, + ESource *source) +{ + GHashTable *pending_writes; + + /* Cancel any pending writes for this ESource so as not + * to overwrite whatever change we're being notified of. */ + pending_writes = selector->priv->pending_writes; + g_hash_table_remove (pending_writes, source); +} + +static void +source_selector_update_row (ESourceSelector *selector, + ESource *source) +{ + GHashTable *source_index; + ESourceExtension *extension = NULL; + GtkTreeRowReference *reference; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + const gchar *extension_name; + const gchar *display_name; + gboolean selected; + + source_index = selector->priv->source_index; + reference = g_hash_table_lookup (source_index, source); + + /* This function runs when ANY ESource in the registry changes. + * If the ESource is not in our tree model then return silently. */ + if (reference == NULL) + return; + + /* If we do have a row reference, it should be valid. */ + g_return_if_fail (gtk_tree_row_reference_valid (reference)); + + model = gtk_tree_row_reference_get_model (reference); + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + + display_name = e_source_get_display_name (source); + + extension_name = e_source_selector_get_extension_name (selector); + selected = e_source_selector_source_is_selected (selector, source); + + if (e_source_has_extension (source, extension_name)) + extension = e_source_get_extension (source, extension_name); + + if (extension != NULL) { + GdkColor color; + const gchar *color_spec = NULL; + gboolean show_color = FALSE; + gboolean show_toggle; + + show_color = + E_IS_SOURCE_SELECTABLE (extension) && + e_source_selector_get_show_colors (selector); + + if (show_color) + color_spec = e_source_selectable_get_color ( + E_SOURCE_SELECTABLE (extension)); + + if (color_spec != NULL && *color_spec != '\0') + show_color = gdk_color_parse (color_spec, &color); + + show_toggle = e_source_selector_get_show_toggles (selector); + + gtk_tree_store_set ( + GTK_TREE_STORE (model), &iter, + COLUMN_NAME, display_name, + COLUMN_COLOR, show_color ? &color : NULL, + COLUMN_ACTIVE, selected, + COLUMN_SHOW_COLOR, show_color, + COLUMN_SHOW_TOGGLE, show_toggle, + COLUMN_WEIGHT, PANGO_WEIGHT_NORMAL, + COLUMN_SOURCE, source, + -1); + } else { + gtk_tree_store_set ( + GTK_TREE_STORE (model), &iter, + COLUMN_NAME, display_name, + COLUMN_COLOR, NULL, + COLUMN_ACTIVE, FALSE, + COLUMN_SHOW_COLOR, FALSE, + COLUMN_SHOW_TOGGLE, FALSE, + COLUMN_WEIGHT, PANGO_WEIGHT_BOLD, + COLUMN_SOURCE, source, + -1); + } +} + +static gboolean +source_selector_traverse (GNode *node, + ESourceSelector *selector) +{ + ESource *source; + GHashTable *source_index; + GtkTreeRowReference *reference = NULL; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + + /* Skip the root node. */ + if (G_NODE_IS_ROOT (node)) + return FALSE; + + source_index = selector->priv->source_index; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (selector)); + + if (node->parent != NULL && node->parent->data != NULL) + reference = g_hash_table_lookup ( + source_index, node->parent->data); + + if (gtk_tree_row_reference_valid (reference)) { + GtkTreeIter parent; + + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_model_get_iter (model, &parent, path); + gtk_tree_path_free (path); + + gtk_tree_store_append (GTK_TREE_STORE (model), &iter, &parent); + } else + gtk_tree_store_append (GTK_TREE_STORE (model), &iter, NULL); + + source = E_SOURCE (node->data); + + path = gtk_tree_model_get_path (model, &iter); + reference = gtk_tree_row_reference_new (model, path); + g_hash_table_insert (source_index, g_object_ref (source), reference); + gtk_tree_path_free (path); + + source_selector_update_row (selector, source); + + return FALSE; +} + +static void +source_selector_save_expanded (GtkTreeView *tree_view, + GtkTreePath *path, + GQueue *queue) +{ + GtkTreeModel *model; + GtkTreeIter iter; + ESource *source; + + model = gtk_tree_view_get_model (tree_view); + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + g_queue_push_tail (queue, source); +} + +static void +source_selector_build_model (ESourceSelector *selector) +{ + ESourceRegistry *registry; + GQueue queue = G_QUEUE_INIT; + GHashTable *source_index; + GtkTreeView *tree_view; + GtkTreeModel *model; + ESource *selected; + const gchar *extension_name; + GNode *root; + + tree_view = GTK_TREE_VIEW (selector); + + registry = e_source_selector_get_registry (selector); + extension_name = e_source_selector_get_extension_name (selector); + + /* Make sure we have what we need to build the model, since + * this can get called early in the initialization phase. */ + if (registry == NULL || extension_name == NULL) + return; + + source_index = selector->priv->source_index; + selected = e_source_selector_ref_primary_selection (selector); + + /* Save expanded sources to restore later. */ + gtk_tree_view_map_expanded_rows ( + tree_view, (GtkTreeViewMappingFunc) + source_selector_save_expanded, &queue); + + model = gtk_tree_view_get_model (tree_view); + gtk_tree_store_clear (GTK_TREE_STORE (model)); + + g_hash_table_remove_all (source_index); + + root = e_source_registry_build_display_tree (registry, extension_name); + + g_node_traverse ( + root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + (GNodeTraverseFunc) source_selector_traverse, + selector); + + e_source_registry_free_display_tree (root); + + /* Restore previously expanded sources. */ + while (!g_queue_is_empty (&queue)) { + GtkTreeRowReference *reference; + ESource *source; + + source = g_queue_pop_head (&queue); + reference = g_hash_table_lookup (source_index, source); + + if (gtk_tree_row_reference_valid (reference)) { + GtkTreePath *path; + + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_view_expand_to_path (tree_view, path); + gtk_tree_path_free (path); + } + + g_object_unref (source); + } + + /* Restore the primary selection. */ + if (selected != NULL) { + e_source_selector_set_primary_selection (selector, selected); + g_object_unref (selected); + } + + /* Make sure we have a primary selection. If not, pick one. */ + selected = e_source_selector_ref_primary_selection (selector); + if (selected == NULL) { + selected = e_source_registry_ref_default_for_extension_name ( + registry, extension_name); + e_source_selector_set_primary_selection (selector, selected); + } + g_object_unref (selected); +} + +static void +source_selector_expand_to_source (ESourceSelector *selector, + ESource *source) +{ + GHashTable *source_index; + GtkTreeRowReference *reference; + GtkTreePath *path; + + source_index = selector->priv->source_index; + reference = g_hash_table_lookup (source_index, source); + + /* If the ESource is not in our tree model then return silently. */ + if (reference == NULL) + return; + + /* If we do have a row reference, it should be valid. */ + g_return_if_fail (gtk_tree_row_reference_valid (reference)); + + /* Expand the tree view to the path containing the ESource */ + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_view_expand_to_path (GTK_TREE_VIEW (selector), path); + gtk_tree_path_free (path); +} + +static void +source_selector_source_added_cb (ESourceRegistry *registry, + ESource *source, + ESourceSelector *selector) +{ + source_selector_build_model (selector); + + source_selector_expand_to_source (selector, source); +} + +static void +source_selector_source_changed_cb (ESourceRegistry *registry, + ESource *source, + ESourceSelector *selector) +{ + source_selector_cancel_write (selector, source); + + source_selector_update_row (selector, source); +} + +static void +source_selector_source_removed_cb (ESourceRegistry *registry, + ESource *source, + ESourceSelector *selector) +{ + source_selector_build_model (selector); +} + +static void +source_selector_source_enabled_cb (ESourceRegistry *registry, + ESource *source, + ESourceSelector *selector) +{ + source_selector_build_model (selector); + + source_selector_expand_to_source (selector, source); +} + +static void +source_selector_source_disabled_cb (ESourceRegistry *registry, + ESource *source, + ESourceSelector *selector) +{ + source_selector_build_model (selector); +} + +static gboolean +same_source_name_exists (ESourceSelector *selector, + const gchar *display_name) +{ + GHashTable *source_index; + GHashTableIter iter; + gpointer key; + + source_index = selector->priv->source_index; + g_hash_table_iter_init (&iter, source_index); + + while (g_hash_table_iter_next (&iter, &key, NULL)) { + ESource *source = E_SOURCE (key); + const gchar *source_name; + + source_name = e_source_get_display_name (source); + if (g_strcmp0 (display_name, source_name) == 0) + return TRUE; + } + + return FALSE; +} + +static gboolean +selection_func (GtkTreeSelection *selection, + GtkTreeModel *model, + GtkTreePath *path, + gboolean path_currently_selected, + ESourceSelector *selector) +{ + ESource *source; + GtkTreeIter iter; + const gchar *extension_name; + + if (selector->priv->toggled_last) { + selector->priv->toggled_last = FALSE; + return FALSE; + } + + if (path_currently_selected) + return TRUE; + + if (!gtk_tree_model_get_iter (model, &iter, path)) + return FALSE; + + extension_name = e_source_selector_get_extension_name (selector); + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + + if (!e_source_has_extension (source, extension_name)) { + g_object_unref (source); + return FALSE; + } + + clear_saved_primary_selection (selector); + + g_object_unref (source); + + return TRUE; +} + +static void +text_cell_edited_cb (ESourceSelector *selector, + const gchar *path_string, + const gchar *new_name) +{ + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + ESource *source; + + tree_view = GTK_TREE_VIEW (selector); + model = gtk_tree_view_get_model (tree_view); + path = gtk_tree_path_new_from_string (path_string); + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + gtk_tree_path_free (path); + + if (new_name == NULL || *new_name == '\0') + return; + + if (same_source_name_exists (selector, new_name)) + return; + + e_source_set_display_name (source, new_name); + + e_source_selector_queue_write (selector, source); +} + +static void +cell_toggled_callback (GtkCellRendererToggle *renderer, + const gchar *path_string, + ESourceSelector *selector) +{ + ESource *source; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (selector)); + path = gtk_tree_path_new_from_string (path_string); + + if (!gtk_tree_model_get_iter (model, &iter, path)) { + gtk_tree_path_free (path); + return; + } + + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + + if (e_source_selector_source_is_selected (selector, source)) + e_source_selector_unselect_source (selector, source); + else + e_source_selector_select_source (selector, source); + + selector->priv->toggled_last = TRUE; + + gtk_tree_path_free (path); + + g_object_unref (source); +} + +static void +selection_changed_callback (GtkTreeSelection *selection, + ESourceSelector *selector) +{ + g_signal_emit (selector, signals[PRIMARY_SELECTION_CHANGED], 0); + g_object_notify (G_OBJECT (selector), "primary-selection"); +} + +static void +source_selector_set_extension_name (ESourceSelector *selector, + const gchar *extension_name) +{ + g_return_if_fail (extension_name != NULL); + g_return_if_fail (selector->priv->extension_name == NULL); + + selector->priv->extension_name = g_strdup (extension_name); +} + +static void +source_selector_set_registry (ESourceSelector *selector, + ESourceRegistry *registry) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (selector->priv->registry == NULL); + + selector->priv->registry = g_object_ref (registry); +} + +static void +source_selector_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_EXTENSION_NAME: + source_selector_set_extension_name ( + E_SOURCE_SELECTOR (object), + g_value_get_string (value)); + return; + + case PROP_PRIMARY_SELECTION: + e_source_selector_set_primary_selection ( + E_SOURCE_SELECTOR (object), + g_value_get_object (value)); + return; + + case PROP_REGISTRY: + source_selector_set_registry ( + E_SOURCE_SELECTOR (object), + g_value_get_object (value)); + return; + + case PROP_SHOW_COLORS: + e_source_selector_set_show_colors ( + E_SOURCE_SELECTOR (object), + g_value_get_boolean (value)); + return; + + case PROP_SHOW_TOGGLES: + e_source_selector_set_show_toggles ( + E_SOURCE_SELECTOR (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_selector_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_EXTENSION_NAME: + g_value_set_string ( + value, + e_source_selector_get_extension_name ( + E_SOURCE_SELECTOR (object))); + return; + + case PROP_PRIMARY_SELECTION: + g_value_take_object ( + value, + e_source_selector_ref_primary_selection ( + E_SOURCE_SELECTOR (object))); + return; + + case PROP_REGISTRY: + g_value_set_object ( + value, + e_source_selector_get_registry ( + E_SOURCE_SELECTOR (object))); + return; + + case PROP_SHOW_COLORS: + g_value_set_boolean ( + value, + e_source_selector_get_show_colors ( + E_SOURCE_SELECTOR (object))); + return; + + case PROP_SHOW_TOGGLES: + g_value_set_boolean ( + value, + e_source_selector_get_show_toggles ( + E_SOURCE_SELECTOR (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_selector_dispose (GObject *object) +{ + ESourceSelectorPrivate *priv; + + priv = E_SOURCE_SELECTOR_GET_PRIVATE (object); + + if (priv->registry != NULL) { + g_signal_handlers_disconnect_matched ( + priv->registry, + G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->registry); + priv->registry = NULL; + } + + g_hash_table_remove_all (priv->source_index); + g_hash_table_remove_all (priv->pending_writes); + + clear_saved_primary_selection (E_SOURCE_SELECTOR (object)); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_source_selector_parent_class)->dispose (object); +} + +static void +source_selector_finalize (GObject *object) +{ + ESourceSelectorPrivate *priv; + + priv = E_SOURCE_SELECTOR_GET_PRIVATE (object); + + g_hash_table_destroy (priv->source_index); + g_hash_table_destroy (priv->pending_writes); + + g_free (priv->extension_name); + + if (priv->main_context != NULL) + g_main_context_unref (priv->main_context); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_source_selector_parent_class)->finalize (object); +} + +static void +source_selector_constructed (GObject *object) +{ + ESourceRegistry *registry; + ESourceSelector *selector; + + selector = E_SOURCE_SELECTOR (object); + registry = e_source_selector_get_registry (selector); + + g_signal_connect ( + registry, "source-added", + G_CALLBACK (source_selector_source_added_cb), selector); + + g_signal_connect ( + registry, "source-changed", + G_CALLBACK (source_selector_source_changed_cb), selector); + + g_signal_connect ( + registry, "source-removed", + G_CALLBACK (source_selector_source_removed_cb), selector); + + g_signal_connect ( + registry, "source-enabled", + G_CALLBACK (source_selector_source_enabled_cb), selector); + + g_signal_connect ( + registry, "source-disabled", + G_CALLBACK (source_selector_source_disabled_cb), selector); + + source_selector_build_model (selector); + + gtk_tree_view_expand_all (GTK_TREE_VIEW (selector)); +} + +static gboolean +source_selector_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + ESourceSelector *selector; + GtkWidgetClass *widget_class; + GtkTreePath *path; + ESource *source = NULL; + ESource *primary; + gboolean right_click = FALSE; + gboolean triple_click = FALSE; + gboolean row_exists; + gboolean res = FALSE; + + selector = E_SOURCE_SELECTOR (widget); + + selector->priv->toggled_last = FALSE; + + /* Triple-clicking a source selects it exclusively. */ + + if (event->button == 3 && event->type == GDK_BUTTON_PRESS) + right_click = TRUE; + else if (event->button == 1 && event->type == GDK_3BUTTON_PRESS) + triple_click = TRUE; + else + goto chainup; + + row_exists = gtk_tree_view_get_path_at_pos ( + GTK_TREE_VIEW (widget), event->x, event->y, + &path, NULL, NULL, NULL); + + /* Get the source/group */ + if (row_exists) { + GtkTreeModel *model; + GtkTreeIter iter; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget)); + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + } + + if (source == NULL) + goto chainup; + + primary = e_source_selector_ref_primary_selection (selector); + if (source != primary) + e_source_selector_set_primary_selection (selector, source); + if (primary != NULL) + g_object_unref (primary); + + if (right_click) + g_signal_emit ( + widget, signals[POPUP_EVENT], 0, source, event, &res); + + if (triple_click) { + e_source_selector_select_exclusive (selector, source); + res = TRUE; + } + + g_object_unref (source); + + return res; + +chainup: + + /* Chain up to parent's button_press_event() method. */ + widget_class = GTK_WIDGET_CLASS (e_source_selector_parent_class); + return widget_class->button_press_event (widget, event); +} + +static void +source_selector_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time_) +{ + GtkTreeView *tree_view; + GtkTreeViewDropPosition pos; + + tree_view = GTK_TREE_VIEW (widget); + pos = GTK_TREE_VIEW_DROP_BEFORE; + + gtk_tree_view_set_drag_dest_row (tree_view, NULL, pos); +} + +static gboolean +source_selector_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time_) +{ + ESource *source = NULL; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreePath *path = NULL; + GtkTreeIter iter; + GtkTreeViewDropPosition pos; + GdkDragAction action = 0; + + tree_view = GTK_TREE_VIEW (widget); + model = gtk_tree_view_get_model (tree_view); + + if (!gtk_tree_view_get_dest_row_at_pos (tree_view, x, y, &path, NULL)) + goto exit; + + if (!gtk_tree_model_get_iter (model, &iter, path)) + goto exit; + + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + + if (!e_source_get_writable (source)) + goto exit; + + pos = GTK_TREE_VIEW_DROP_INTO_OR_BEFORE; + gtk_tree_view_set_drag_dest_row (tree_view, path, pos); + + if (gdk_drag_context_get_actions (context) & GDK_ACTION_MOVE) + action = GDK_ACTION_MOVE; + else + action = gdk_drag_context_get_suggested_action (context); + +exit: + if (path != NULL) + gtk_tree_path_free (path); + + if (source != NULL) + g_object_unref (source); + + gdk_drag_status (context, action, time_); + + return TRUE; +} + +static gboolean +source_selector_drag_drop (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time_) +{ + ESource *source; + ESourceSelector *selector; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + const gchar *extension_name; + gboolean drop_zone; + gboolean valid; + + tree_view = GTK_TREE_VIEW (widget); + model = gtk_tree_view_get_model (tree_view); + + if (!gtk_tree_view_get_path_at_pos ( + tree_view, x, y, &path, NULL, NULL, NULL)) + return FALSE; + + valid = gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + g_return_val_if_fail (valid, FALSE); + + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + + selector = E_SOURCE_SELECTOR (widget); + extension_name = e_source_selector_get_extension_name (selector); + drop_zone = e_source_has_extension (source, extension_name); + + g_object_unref (source); + + return drop_zone; +} + +static void +source_selector_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time_) +{ + ESource *source = NULL; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreePath *path = NULL; + GtkTreeIter iter; + GdkDragAction action; + gboolean delete; + gboolean success = FALSE; + + tree_view = GTK_TREE_VIEW (widget); + model = gtk_tree_view_get_model (tree_view); + + action = gdk_drag_context_get_selected_action (context); + delete = (action == GDK_ACTION_MOVE); + + if (!gtk_tree_view_get_dest_row_at_pos (tree_view, x, y, &path, NULL)) + goto exit; + + if (!gtk_tree_model_get_iter (model, &iter, path)) + goto exit; + + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + + if (!e_source_get_writable (source)) + goto exit; + + g_signal_emit ( + widget, signals[DATA_DROPPED], 0, selection_data, + source, gdk_drag_context_get_selected_action (context), + info, &success); + +exit: + if (path != NULL) + gtk_tree_path_free (path); + + if (source != NULL) + g_object_unref (source); + + gtk_drag_finish (context, success, delete, time_); +} + +static gboolean +source_selector_popup_menu (GtkWidget *widget) +{ + ESourceSelector *selector; + ESource *source; + gboolean res = FALSE; + + selector = E_SOURCE_SELECTOR (widget); + source = e_source_selector_ref_primary_selection (selector); + g_signal_emit (selector, signals[POPUP_EVENT], 0, source, NULL, &res); + + if (source != NULL) + g_object_unref (source); + + return res; +} + +static gboolean +source_selector_test_collapse_row (GtkTreeView *tree_view, + GtkTreeIter *iter, + GtkTreePath *path) +{ + ESourceSelectorPrivate *priv; + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreeIter child_iter; + + priv = E_SOURCE_SELECTOR_GET_PRIVATE (tree_view); + + /* Clear this because something else has been clicked on now */ + priv->toggled_last = FALSE; + + if (priv->saved_primary_selection) + return FALSE; + + selection = gtk_tree_view_get_selection (tree_view); + + if (!gtk_tree_selection_get_selected (selection, &model, &child_iter)) + return FALSE; + + if (gtk_tree_store_is_ancestor (GTK_TREE_STORE (model), iter, &child_iter)) { + GtkTreeRowReference *reference; + GtkTreePath *child_path; + + child_path = gtk_tree_model_get_path (model, &child_iter); + reference = gtk_tree_row_reference_new (model, child_path); + priv->saved_primary_selection = reference; + gtk_tree_path_free (child_path); + } + + return FALSE; +} + +static void +source_selector_row_expanded (GtkTreeView *tree_view, + GtkTreeIter *iter, + GtkTreePath *path) +{ + ESourceSelectorPrivate *priv; + GtkTreeModel *model; + GtkTreePath *child_path; + GtkTreeIter child_iter; + + priv = E_SOURCE_SELECTOR_GET_PRIVATE (tree_view); + + if (!priv->saved_primary_selection) + return; + + model = gtk_tree_view_get_model (tree_view); + + child_path = gtk_tree_row_reference_get_path ( + priv->saved_primary_selection); + gtk_tree_model_get_iter (model, &child_iter, child_path); + + if (gtk_tree_store_is_ancestor (GTK_TREE_STORE (model), iter, &child_iter)) { + GtkTreeSelection *selection; + + selection = gtk_tree_view_get_selection (tree_view); + gtk_tree_selection_select_iter (selection, &child_iter); + + clear_saved_primary_selection (E_SOURCE_SELECTOR (tree_view)); + } + + gtk_tree_path_free (child_path); +} + +static gboolean +source_selector_get_source_selected (ESourceSelector *selector, + ESource *source) +{ + ESourceSelectable *extension; + const gchar *extension_name; + gboolean selected = TRUE; + + extension_name = e_source_selector_get_extension_name (selector); + + if (!e_source_has_extension (source, extension_name)) + return FALSE; + + extension = e_source_get_extension (source, extension_name); + + if (E_IS_SOURCE_SELECTABLE (extension)) + selected = e_source_selectable_get_selected (extension); + + return selected; +} + +static void +source_selector_set_source_selected (ESourceSelector *selector, + ESource *source, + gboolean selected) +{ + ESourceSelectable *extension; + const gchar *extension_name; + + extension_name = e_source_selector_get_extension_name (selector); + + if (!e_source_has_extension (source, extension_name)) + return; + + extension = e_source_get_extension (source, extension_name); + + if (!E_IS_SOURCE_SELECTABLE (extension)) + return; + + if (selected != e_source_selectable_get_selected (extension)) { + e_source_selectable_set_selected (extension, selected); + e_source_selector_queue_write (selector, source); + } +} + +static gboolean +ess_bool_accumulator (GSignalInvocationHint *ihint, + GValue *out, + const GValue *in, + gpointer data) +{ + gboolean v_boolean; + + v_boolean = g_value_get_boolean (in); + g_value_set_boolean (out, v_boolean); + + return !v_boolean; +} + +static void +e_source_selector_class_init (ESourceSelectorClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkTreeViewClass *tree_view_class; + + g_type_class_add_private (class, sizeof (ESourceSelectorPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = source_selector_set_property; + object_class->get_property = source_selector_get_property; + object_class->dispose = source_selector_dispose; + object_class->finalize = source_selector_finalize; + object_class->constructed = source_selector_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->button_press_event = source_selector_button_press_event; + widget_class->drag_leave = source_selector_drag_leave; + widget_class->drag_motion = source_selector_drag_motion; + widget_class->drag_drop = source_selector_drag_drop; + widget_class->drag_data_received = source_selector_drag_data_received; + widget_class->popup_menu = source_selector_popup_menu; + + tree_view_class = GTK_TREE_VIEW_CLASS (class); + tree_view_class->test_collapse_row = source_selector_test_collapse_row; + tree_view_class->row_expanded = source_selector_row_expanded; + + class->get_source_selected = source_selector_get_source_selected; + class->set_source_selected = source_selector_set_source_selected; + + g_object_class_install_property ( + object_class, + PROP_EXTENSION_NAME, + g_param_spec_string ( + "extension-name", + NULL, + NULL, + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_PRIMARY_SELECTION, + g_param_spec_object ( + "primary-selection", + NULL, + NULL, + E_TYPE_SOURCE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + NULL, + NULL, + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_COLORS, + g_param_spec_boolean ( + "show-colors", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SHOW_TOGGLES, + g_param_spec_boolean ( + "show-toggles", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + signals[SELECTION_CHANGED] = g_signal_new ( + "selection-changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESourceSelectorClass, selection_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /* XXX Consider this signal deprecated. Connect + * to "notify::primary-selection" instead. */ + signals[PRIMARY_SELECTION_CHANGED] = g_signal_new ( + "primary-selection-changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESourceSelectorClass, primary_selection_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[POPUP_EVENT] = g_signal_new ( + "popup-event", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESourceSelectorClass, popup_event), + ess_bool_accumulator, NULL, NULL, + G_TYPE_BOOLEAN, 2, G_TYPE_OBJECT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + signals[DATA_DROPPED] = g_signal_new ( + "data-dropped", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESourceSelectorClass, data_dropped), + NULL, NULL, NULL, + G_TYPE_BOOLEAN, 4, + GTK_TYPE_SELECTION_DATA | G_SIGNAL_TYPE_STATIC_SCOPE, + E_TYPE_SOURCE, + GDK_TYPE_DRAG_ACTION, + G_TYPE_UINT); +} + +static void +e_source_selector_init (ESourceSelector *selector) +{ + GHashTable *pending_writes; + GtkTreeViewColumn *column; + GtkTreeSelection *selection; + GtkCellRenderer *renderer; + GtkTreeStore *tree_store; + GtkTreeView *tree_view; + + pending_writes = g_hash_table_new_full ( + (GHashFunc) g_direct_hash, + (GEqualFunc) g_direct_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) pending_writes_destroy_source); + + selector->priv = E_SOURCE_SELECTOR_GET_PRIVATE (selector); + + selector->priv->pending_writes = pending_writes; + + selector->priv->main_context = g_main_context_get_thread_default (); + if (selector->priv->main_context != NULL) + g_main_context_ref (selector->priv->main_context); + + tree_view = GTK_TREE_VIEW (selector); + + gtk_tree_view_set_search_column (tree_view, COLUMN_SOURCE); + gtk_tree_view_set_enable_search (tree_view, TRUE); + + selector->priv->toggled_last = FALSE; + selector->priv->select_new = FALSE; + selector->priv->show_colors = TRUE; + selector->priv->show_toggles = TRUE; + + selector->priv->source_index = g_hash_table_new_full ( + (GHashFunc) e_source_hash, + (GEqualFunc) e_source_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) gtk_tree_row_reference_free); + + tree_store = gtk_tree_store_new ( + NUM_COLUMNS, + G_TYPE_STRING, /* COLUMN_NAME */ + GDK_TYPE_COLOR, /* COLUMN_COLOR */ + G_TYPE_BOOLEAN, /* COLUMN_ACTIVE */ + G_TYPE_BOOLEAN, /* COLUMN_SHOW_COLOR */ + G_TYPE_BOOLEAN, /* COLUMN_SHOW_TOGGLE */ + G_TYPE_INT, /* COLUMN_WEIGHT */ + E_TYPE_SOURCE); /* COLUMN_SOURCE */ + + gtk_tree_view_set_model (tree_view, GTK_TREE_MODEL (tree_store)); + + column = gtk_tree_view_column_new (); + gtk_tree_view_append_column (tree_view, column); + + renderer = e_cell_renderer_color_new (); + g_object_set ( + G_OBJECT (renderer), "mode", + GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + gtk_tree_view_column_add_attribute ( + column, renderer, "color", COLUMN_COLOR); + gtk_tree_view_column_add_attribute ( + column, renderer, "visible", COLUMN_SHOW_COLOR); + + renderer = e_cell_renderer_safe_toggle_new (); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + gtk_tree_view_column_add_attribute ( + column, renderer, "active", COLUMN_ACTIVE); + gtk_tree_view_column_add_attribute ( + column, renderer, "visible", COLUMN_SHOW_TOGGLE); + g_signal_connect ( + renderer, "toggled", + G_CALLBACK (cell_toggled_callback), selector); + + renderer = gtk_cell_renderer_text_new (); + g_object_set ( + G_OBJECT (renderer), + "ellipsize", PANGO_ELLIPSIZE_END, NULL); + g_signal_connect_swapped ( + renderer, "edited", + G_CALLBACK (text_cell_edited_cb), selector); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + gtk_tree_view_column_set_attributes ( + column, renderer, + "text", COLUMN_NAME, + "weight", COLUMN_WEIGHT, + NULL); + + selection = gtk_tree_view_get_selection (tree_view); + gtk_tree_selection_set_select_function ( + selection, (GtkTreeSelectionFunc) + selection_func, selector, NULL); + g_signal_connect_object ( + selection, "changed", + G_CALLBACK (selection_changed_callback), + G_OBJECT (selector), 0); + + gtk_tree_view_set_headers_visible (tree_view, FALSE); +} + +/** + * e_source_selector_new: + * @registry: an #ESourceRegistry + * @extension_name: the name of an #ESource extension + * + * Displays a list of sources from @registry having an extension named + * @extension_name. The sources are grouped by backend or groupware + * account, which are described by the parent source. + * + * Returns: a new #ESourceSelector + **/ +GtkWidget * +e_source_selector_new (ESourceRegistry *registry, + const gchar *extension_name) +{ + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + g_return_val_if_fail (extension_name != NULL, NULL); + + return g_object_new ( + E_TYPE_SOURCE_SELECTOR, "registry", registry, + "extension-name", extension_name, NULL); +} + +/** + * e_source_selector_get_registry: + * @selector: an #ESourceSelector + * + * Returns the #ESourceRegistry that @selector is getting sources from. + * + * Returns: an #ESourceRegistry + * + * Since: 3.6 + **/ +ESourceRegistry * +e_source_selector_get_registry (ESourceSelector *selector) +{ + g_return_val_if_fail (E_IS_SOURCE_SELECTOR (selector), NULL); + + return selector->priv->registry; +} + +/** + * e_source_selector_get_extension_name: + * @selector: an #ESourceSelector + * + * Returns the extension name used to filter which sources are displayed. + * + * Returns: the #ESource extension name + * + * Since: 3.6 + **/ +const gchar * +e_source_selector_get_extension_name (ESourceSelector *selector) +{ + g_return_val_if_fail (E_IS_SOURCE_SELECTOR (selector), NULL); + + return selector->priv->extension_name; +} + +/** + * e_source_selector_get_show_colors: + * @selector: an #ESourceSelector + * + * Returns whether colors are shown next to data sources. + * + * Returns: %TRUE if colors are being shown + * + * Since: 3.6 + **/ +gboolean +e_source_selector_get_show_colors (ESourceSelector *selector) +{ + g_return_val_if_fail (E_IS_SOURCE_SELECTOR (selector), FALSE); + + return selector->priv->show_colors; +} + +/** + * e_source_selector_set_show_colors: + * @selector: an #ESourceSelector + * @show_colors: whether to show colors + * + * Sets whether to show colors next to data sources. + * + * Since: 3.6 + **/ +void +e_source_selector_set_show_colors (ESourceSelector *selector, + gboolean show_colors) +{ + g_return_if_fail (E_IS_SOURCE_SELECTOR (selector)); + + if ((show_colors ? 1 : 0) == (selector->priv->show_colors ? 1 : 0)) + return; + + selector->priv->show_colors = show_colors; + + g_object_notify (G_OBJECT (selector), "show-colors"); + + source_selector_build_model (selector); +} + +/** + * e_source_selector_get_show_toggles: + * @selector: an #ESourceSelector + * + * Returns whether toggles are shown next to data sources. + * + * Returns: %TRUE if toggles are being shown + * + * Since: 3.6 + **/ +gboolean +e_source_selector_get_show_toggles (ESourceSelector *selector) +{ + g_return_val_if_fail (E_IS_SOURCE_SELECTOR (selector), FALSE); + + return selector->priv->show_toggles; +} + +/** + * e_source_selector_set_show_toggles: + * @selector: an #ESourceSelector + * @show_toggles: whether to show toggles + * + * Sets whether to show toggles next to data sources. + * + * Since: 3.6 + **/ +void +e_source_selector_set_show_toggles (ESourceSelector *selector, + gboolean show_toggles) +{ + g_return_if_fail (E_IS_SOURCE_SELECTOR (selector)); + + if ((show_toggles ? 1 : 0) == (selector->priv->show_toggles ? 1 : 0)) + return; + + selector->priv->show_toggles = show_toggles; + + g_object_notify (G_OBJECT (selector), "show-toggles"); + + source_selector_build_model (selector); +} + +/* Helper for e_source_selector_get_selection() */ +static gboolean +source_selector_check_selected (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + ESource *source; + + struct { + ESourceSelector *selector; + GSList *list; + } *closure = user_data; + + gtk_tree_model_get (model, iter, COLUMN_SOURCE, &source, -1); + + if (e_source_selector_source_is_selected (closure->selector, source)) + closure->list = g_slist_prepend (closure->list, source); + else + g_object_unref (source); + + return FALSE; +} + +/** + * e_source_selector_get_selection: + * @selector: an #ESourceSelector + * + * Get the list of selected sources, i.e. those that were enabled through the + * corresponding checkboxes in the tree. + * + * Returns: A list of the ESources currently selected. The sources will + * be in the same order as they appear on the screen, and the list should be + * freed using e_source_selector_free_selection(). + **/ +GSList * +e_source_selector_get_selection (ESourceSelector *selector) +{ + struct { + ESourceSelector *selector; + GSList *list; + } closure; + + g_return_val_if_fail (E_IS_SOURCE_SELECTOR (selector), NULL); + + closure.selector = selector; + closure.list = NULL; + + gtk_tree_model_foreach ( + gtk_tree_view_get_model (GTK_TREE_VIEW (selector)), + (GtkTreeModelForeachFunc) source_selector_check_selected, + &closure); + + return g_slist_reverse (closure.list); +} + +/** + * e_source_list_free_selection: + * @list: A selection list returned by e_source_selector_get_selection(). + * + * Free the selection list. + **/ +void +e_source_selector_free_selection (GSList *list) +{ + g_slist_foreach (list, (GFunc) g_object_unref, NULL); + g_slist_free (list); +} + +/** + * e_source_selector_set_select_new: + * @selector: An #ESourceSelector widget + * @state: A gboolean + * + * Set whether or not to select new sources added to @selector. + **/ +void +e_source_selector_set_select_new (ESourceSelector *selector, + gboolean state) +{ + g_return_if_fail (E_IS_SOURCE_SELECTOR (selector)); + + selector->priv->select_new = state; +} + +/** + * e_source_selector_select_source: + * @selector: An #ESourceSelector widget + * @source: An #ESource. + * + * Select @source in @selector. + **/ +void +e_source_selector_select_source (ESourceSelector *selector, + ESource *source) +{ + ESourceSelectorClass *class; + GtkTreeRowReference *reference; + GHashTable *source_index; + + g_return_if_fail (E_IS_SOURCE_SELECTOR (selector)); + g_return_if_fail (E_IS_SOURCE (source)); + + /* Make sure the ESource is in our tree model. */ + source_index = selector->priv->source_index; + reference = g_hash_table_lookup (source_index, source); + g_return_if_fail (gtk_tree_row_reference_valid (reference)); + + class = E_SOURCE_SELECTOR_GET_CLASS (selector); + g_return_if_fail (class->set_source_selected != NULL); + + class->set_source_selected (selector, source, TRUE); + + g_signal_emit (selector, signals[SELECTION_CHANGED], 0); +} + +/** + * e_source_selector_unselect_source: + * @selector: An #ESourceSelector widget + * @source: An #ESource. + * + * Unselect @source in @selector. + **/ +void +e_source_selector_unselect_source (ESourceSelector *selector, + ESource *source) +{ + ESourceSelectorClass *class; + GtkTreeRowReference *reference; + GHashTable *source_index; + + g_return_if_fail (E_IS_SOURCE_SELECTOR (selector)); + g_return_if_fail (E_IS_SOURCE (source)); + + /* Make sure the ESource is in our tree model. */ + source_index = selector->priv->source_index; + reference = g_hash_table_lookup (source_index, source); + g_return_if_fail (gtk_tree_row_reference_valid (reference)); + + class = E_SOURCE_SELECTOR_GET_CLASS (selector); + g_return_if_fail (class->set_source_selected != NULL); + + class->set_source_selected (selector, source, FALSE); + + g_signal_emit (selector, signals[SELECTION_CHANGED], 0); +} + +/** + * e_source_selector_select_exclusive: + * @selector: An #ESourceSelector widget + * @source: An #ESource. + * + * Select @source in @selector and unselect all others. + * + * Since: 2.30 + **/ +void +e_source_selector_select_exclusive (ESourceSelector *selector, + ESource *source) +{ + ESourceSelectorClass *class; + GHashTable *source_index; + GHashTableIter iter; + gpointer key; + + g_return_if_fail (E_IS_SOURCE_SELECTOR (selector)); + g_return_if_fail (E_IS_SOURCE (source)); + + class = E_SOURCE_SELECTOR_GET_CLASS (selector); + g_return_if_fail (class->set_source_selected != NULL); + + source_index = selector->priv->source_index; + g_hash_table_iter_init (&iter, source_index); + + while (g_hash_table_iter_next (&iter, &key, NULL)) { + gboolean selected = e_source_equal (key, source); + class->set_source_selected (selector, key, selected); + } + + g_signal_emit (selector, signals[SELECTION_CHANGED], 0); +} + +/** + * e_source_selector_source_is_selected: + * @selector: An #ESourceSelector widget + * @source: An #ESource. + * + * Check whether @source is selected in @selector. + * + * Returns: %TRUE if @source is currently selected, %FALSE otherwise. + **/ +gboolean +e_source_selector_source_is_selected (ESourceSelector *selector, + ESource *source) +{ + ESourceSelectorClass *class; + GtkTreeRowReference *reference; + GHashTable *source_index; + + g_return_val_if_fail (E_IS_SOURCE_SELECTOR (selector), FALSE); + g_return_val_if_fail (E_IS_SOURCE (source), FALSE); + + /* Make sure the ESource is in our tree model. */ + source_index = selector->priv->source_index; + reference = g_hash_table_lookup (source_index, source); + g_return_val_if_fail (gtk_tree_row_reference_valid (reference), FALSE); + + class = E_SOURCE_SELECTOR_GET_CLASS (selector); + g_return_val_if_fail (class->get_source_selected != NULL, FALSE); + + return class->get_source_selected (selector, source); +} + +/** + * e_source_selector_edit_primary_selection: + * @selector: An #ESourceSelector widget + * + * Allows the user to rename the primary selected source by opening an + * entry box directly in @selector. + * + * Since: 2.26 + **/ +void +e_source_selector_edit_primary_selection (ESourceSelector *selector) +{ + GtkTreeRowReference *reference; + GtkTreeSelection *selection; + GtkTreeViewColumn *column; + GtkCellRenderer *renderer; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreePath *path = NULL; + GtkTreeIter iter; + GList *list; + + g_return_if_fail (E_IS_SOURCE_SELECTOR (selector)); + + tree_view = GTK_TREE_VIEW (selector); + column = gtk_tree_view_get_column (tree_view, 0); + reference = selector->priv->saved_primary_selection; + selection = gtk_tree_view_get_selection (tree_view); + + if (reference != NULL) + path = gtk_tree_row_reference_get_path (reference); + else if (gtk_tree_selection_get_selected (selection, &model, &iter)) + path = gtk_tree_model_get_path (model, &iter); + + if (path == NULL) + return; + + /* XXX Because we stuff three renderers in a single column, + * we have to manually hunt for the text renderer. */ + renderer = NULL; + list = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (column)); + while (list != NULL) { + renderer = list->data; + if (GTK_IS_CELL_RENDERER_TEXT (renderer)) + break; + list = g_list_delete_link (list, list); + } + g_list_free (list); + + /* Make the text cell renderer editable, but only temporarily. + * We don't want editing to be activated by simply clicking on + * the source name. Too easy for accidental edits to occur. */ + g_object_set (renderer, "editable", TRUE, NULL); + gtk_tree_view_expand_to_path (tree_view, path); + gtk_tree_view_set_cursor_on_cell ( + tree_view, path, column, renderer, TRUE); + g_object_set (renderer, "editable", FALSE, NULL); + + gtk_tree_path_free (path); +} + +/** + * e_source_selector_ref_primary_selection: + * @selector: An #ESourceSelector widget + * + * Get the primary selected source. The primary selection is the one that is + * highlighted through the normal #GtkTreeView selection mechanism (as opposed + * to the "normal" selection, which is the set of source whose checkboxes are + * checked). + * + * The returned #ESource is referenced for thread-safety and must be + * unreferenced with g_object_unref() when finished with it. + * + * Returns: The selected source. + * + * Since: 3.6 + **/ +ESource * +e_source_selector_ref_primary_selection (ESourceSelector *selector) +{ + ESource *source; + GtkTreeRowReference *reference; + GtkTreeSelection *selection; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreeIter iter; + const gchar *extension_name; + gboolean have_iter = FALSE; + + g_return_val_if_fail (E_IS_SOURCE_SELECTOR (selector), NULL); + + tree_view = GTK_TREE_VIEW (selector); + model = gtk_tree_view_get_model (tree_view); + selection = gtk_tree_view_get_selection (tree_view); + + reference = selector->priv->saved_primary_selection; + + if (gtk_tree_row_reference_valid (reference)) { + GtkTreePath *path; + + path = gtk_tree_row_reference_get_path (reference); + have_iter = gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + } + + if (!have_iter) + have_iter = gtk_tree_selection_get_selected ( + selection, NULL, &iter); + + if (!have_iter) + return NULL; + + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + + extension_name = e_source_selector_get_extension_name (selector); + + if (!e_source_has_extension (source, extension_name)) { + g_object_unref (source); + return NULL; + } + + return source; +} + +/** + * e_source_selector_set_primary_selection: + * @selector: an #ESourceSelector widget + * @source: an #ESource to select + * + * Highlights @source in @selector. The highlighted #ESource is called + * the primary selection. + * + * Do not confuse this function with e_source_selector_select_source(), + * which activates the check box next to an #ESource's display name in + * @selector. This function does not alter the check box. + **/ +void +e_source_selector_set_primary_selection (ESourceSelector *selector, + ESource *source) +{ + GHashTable *source_index; + GtkTreeRowReference *reference; + GtkTreeSelection *selection; + GtkTreeView *tree_view; + GtkTreePath *child_path; + GtkTreePath *parent_path; + const gchar *extension_name; + + g_return_if_fail (E_IS_SOURCE_SELECTOR (selector)); + g_return_if_fail (E_IS_SOURCE (source)); + + tree_view = GTK_TREE_VIEW (selector); + selection = gtk_tree_view_get_selection (tree_view); + + source_index = selector->priv->source_index; + reference = g_hash_table_lookup (source_index, source); + + /* XXX Maybe we should return a success/fail boolean? */ + if (!gtk_tree_row_reference_valid (reference)) + return; + + extension_name = e_source_selector_get_extension_name (selector); + + /* Return silently if attempting to select a parent node + * lacking the expected extension (e.g. On This Computer). */ + if (!e_source_has_extension (source, extension_name)) + return; + + /* We block the signal because this all needs to be atomic */ + g_signal_handlers_block_matched ( + selection, G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, selection_changed_callback, NULL); + gtk_tree_selection_unselect_all (selection); + g_signal_handlers_unblock_matched ( + selection, G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, selection_changed_callback, NULL); + + clear_saved_primary_selection (selector); + + child_path = gtk_tree_row_reference_get_path (reference); + + parent_path = gtk_tree_path_copy (child_path); + gtk_tree_path_up (parent_path); + + if (gtk_tree_view_row_expanded (tree_view, parent_path)) { + gtk_tree_selection_select_path (selection, child_path); + } else { + selector->priv->saved_primary_selection = + gtk_tree_row_reference_copy (reference); + g_signal_emit (selector, signals[PRIMARY_SELECTION_CHANGED], 0); + g_object_notify (G_OBJECT (selector), "primary-selection"); + } + + gtk_tree_path_free (child_path); + gtk_tree_path_free (parent_path); +} + +/** + * e_source_selector_ref_source_by_path: + * @selector: an #ESourceSelector + * @path: a #GtkTreePath + * + * Returns the #ESource object at @path, or %NULL if @path is invalid. + * + * The returned #ESource is referenced for thread-safety and must be + * unreferenced with g_object_unref() when finished with it. + * + * Returns: the #ESource object at @path, or %NULL + * + * Since: 3.6 + **/ +ESource * +e_source_selector_ref_source_by_path (ESourceSelector *selector, + GtkTreePath *path) +{ + ESource *source = NULL; + GtkTreeModel *model; + GtkTreeIter iter; + + g_return_val_if_fail (E_IS_SOURCE_SELECTOR (selector), NULL); + g_return_val_if_fail (path != NULL, NULL); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (selector)); + + if (gtk_tree_model_get_iter (model, &iter, path)) + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + + return source; +} + +/** + * e_source_selector_queue_write: + * @selector: an #ESourceSelecetor + * @source: an #ESource with changes to be written + * + * Queues a main loop idle callback to write changes to @source back to + * the D-Bus registry service. + * + * Since: 3.6 + **/ +void +e_source_selector_queue_write (ESourceSelector *selector, + ESource *source) +{ + GSource *idle_source; + GHashTable *pending_writes; + GMainContext *main_context; + AsyncContext *async_context; + + g_return_if_fail (E_IS_SOURCE_SELECTOR (selector)); + g_return_if_fail (E_IS_SOURCE (source)); + + main_context = selector->priv->main_context; + pending_writes = selector->priv->pending_writes; + + idle_source = g_hash_table_lookup (pending_writes, source); + if (idle_source != NULL && !g_source_is_destroyed (idle_source)) + return; + + async_context = g_slice_new0 (AsyncContext); + async_context->selector = g_object_ref (selector); + async_context->source = g_object_ref (source); + + /* Set a higher priority so this idle source runs before our + * source_selector_cancel_write() signal handler, which will + * cancel this idle source. Cancellation is the right thing + * to do when receiving changes from OTHER registry clients, + * but we don't want to cancel our own changes. + * + * XXX This might be an argument for using etags. + */ + idle_source = g_idle_source_new (); + g_hash_table_insert ( + pending_writes, + g_object_ref (source), + g_source_ref (idle_source)); + g_source_set_callback ( + idle_source, + source_selector_write_idle_cb, + async_context, + (GDestroyNotify) async_context_free); + g_source_set_priority (idle_source, G_PRIORITY_HIGH_IDLE); + g_source_attach (idle_source, main_context); + g_source_unref (idle_source); +} + diff --git a/e-util/e-source-selector.h b/e-util/e-source-selector.h new file mode 100644 index 0000000000..d4d92284fc --- /dev/null +++ b/e-util/e-source-selector.h @@ -0,0 +1,141 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* e-source-selector.h + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Ettore Perazzoli <ettore@ximian.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SOURCE_SELECTOR_H +#define E_SOURCE_SELECTOR_H + +#include <gtk/gtk.h> +#include <libedataserver/libedataserver.h> + +/* Standard GObject macros */ +#define E_TYPE_SOURCE_SELECTOR \ + (e_source_selector_get_type ()) +#define E_SOURCE_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SOURCE_SELECTOR, ESourceSelector)) +#define E_SOURCE_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SOURCE_SELECTOR, ESourceSelectorClass)) +#define E_IS_SOURCE_SELECTOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SOURCE_SELECTOR)) +#define E_IS_SOURCE_SELECTOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SOURCE_SELECTOR)) +#define E_SOURCE_SELECTOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SOURCE_SELECTOR, ESourceSelectorClass)) + +G_BEGIN_DECLS + +typedef struct _ESourceSelector ESourceSelector; +typedef struct _ESourceSelectorClass ESourceSelectorClass; +typedef struct _ESourceSelectorPrivate ESourceSelectorPrivate; + +struct _ESourceSelector { + GtkTreeView parent; + ESourceSelectorPrivate *priv; +}; + +struct _ESourceSelectorClass { + GtkTreeViewClass parent_class; + + /* Methods */ + gboolean (*get_source_selected) (ESourceSelector *selector, + ESource *source); + void (*set_source_selected) (ESourceSelector *selector, + ESource *source, + gboolean selected); + + /* Signals */ + void (*selection_changed) (ESourceSelector *selector); + void (*primary_selection_changed) + (ESourceSelector *selector); + gboolean (*popup_event) (ESourceSelector *selector, + ESource *primary, + GdkEventButton *event); + gboolean (*data_dropped) (ESourceSelector *selector, + GtkSelectionData *data, + ESource *destination, + GdkDragAction action, + guint target_info); + + gpointer padding1; + gpointer padding2; + gpointer padding3; +}; + +GType e_source_selector_get_type (void); +GtkWidget * e_source_selector_new (ESourceRegistry *registry, + const gchar *extension_name); +ESourceRegistry * + e_source_selector_get_registry (ESourceSelector *selector); +const gchar * e_source_selector_get_extension_name + (ESourceSelector *selector); +gboolean e_source_selector_get_show_colors + (ESourceSelector *selector); +void e_source_selector_set_show_colors + (ESourceSelector *selector, + gboolean show_colors); +gboolean e_source_selector_get_show_toggles + (ESourceSelector *selector); +void e_source_selector_set_show_toggles + (ESourceSelector *selector, + gboolean show_toggles); +void e_source_selector_select_source (ESourceSelector *selector, + ESource *source); +void e_source_selector_unselect_source + (ESourceSelector *selector, + ESource *source); +void e_source_selector_select_exclusive + (ESourceSelector *selector, + ESource *source); +gboolean e_source_selector_source_is_selected + (ESourceSelector *selector, + ESource *source); +GSList * e_source_selector_get_selection (ESourceSelector *selector); +void e_source_selector_free_selection + (GSList *list); +void e_source_selector_set_select_new + (ESourceSelector *selector, + gboolean state); +void e_source_selector_edit_primary_selection + (ESourceSelector *selector); +ESource * e_source_selector_ref_primary_selection + (ESourceSelector *selector); +void e_source_selector_set_primary_selection + (ESourceSelector *selector, + ESource *source); +ESource * e_source_selector_ref_source_by_path + (ESourceSelector *selector, + GtkTreePath *path); +void e_source_selector_queue_write (ESourceSelector *selector, + ESource *source); + +G_END_DECLS + +#endif /* E_SOURCE_SELECTOR_H */ diff --git a/e-util/e-source-util.h b/e-util/e-source-util.h index 452e91113d..e10097f38a 100644 --- a/e-util/e-source-util.h +++ b/e-util/e-source-util.h @@ -16,6 +16,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + /* These functions combine asynchronous ESource and ESourceRegistry methods * with Evolution's EActivity and EAlert facilities to offer an easy-to-use, * "fire-and-forget" API for ESource operations. Use these in situations @@ -28,7 +32,7 @@ #include <libedataserver/libedataserver.h> #include <e-util/e-activity.h> -#include <libevolution-utils/e-alert-sink.h> +#include <e-util/e-alert-sink.h> G_BEGIN_DECLS diff --git a/e-util/e-spell-entry.c b/e-util/e-spell-entry.c new file mode 100644 index 0000000000..56f7c14f8c --- /dev/null +++ b/e-util/e-spell-entry.c @@ -0,0 +1,866 @@ +/* + * e-spell-entry.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +/* This code is based on libsexy's SexySpellEntry */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <glib/gi18n-lib.h> +#include <gtk/gtk.h> + +#include <editor/gtkhtml-spell-language.h> +#include <editor/gtkhtml-spell-checker.h> + +#include "e-spell-entry.h" + +enum { + PROP_0, + PROP_CHECKING_ENABLED +}; + +struct _ESpellEntryPrivate +{ + PangoAttrList *attr_list; + gint mark_character; + gint entry_scroll_offset; + GSettings *settings; + gboolean custom_checkers; + gboolean checking_enabled; + GSList *checkers; + gchar **words; + gint *word_starts; + gint *word_ends; +}; + +#define E_SPELL_ENTRY_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SPELL_ENTRY, ESpellEntryPrivate)) + +G_DEFINE_TYPE (ESpellEntry, e_spell_entry, GTK_TYPE_ENTRY); + +static gboolean +word_misspelled (ESpellEntry *entry, + gint start, + gint end) +{ + const gchar *text; + gchar *word; + gboolean result = TRUE; + + if (start == end) + return FALSE; + + text = gtk_entry_get_text (GTK_ENTRY (entry)); + word = g_new0 (gchar, end - start + 2); + + g_strlcpy (word, text + start, end - start + 1); + + if (g_unichar_isalpha (*word)) { + GSList *li; + gssize wlen = strlen (word); + + for (li = entry->priv->checkers; li; li = g_slist_next (li)) { + GtkhtmlSpellChecker *checker = li->data; + if (gtkhtml_spell_checker_check_word (checker, word, wlen)) { + result = FALSE; + break; + } + } + } + g_free (word); + + return result; +} + +static void +insert_underline (ESpellEntry *entry, + guint start, + guint end) +{ + PangoAttribute *ucolor = pango_attr_underline_color_new (65535, 0, 0); + PangoAttribute *unline = pango_attr_underline_new (PANGO_UNDERLINE_ERROR); + + ucolor->start_index = start; + unline->start_index = start; + + ucolor->end_index = end; + unline->end_index = end; + + pango_attr_list_insert (entry->priv->attr_list, ucolor); + pango_attr_list_insert (entry->priv->attr_list, unline); +} + +static void +check_word (ESpellEntry *entry, + gint start, + gint end) +{ + PangoAttrIterator *it; + + /* Check to see if we've got any attributes at this position. + * If so, free them, since we'll readd it if the word is misspelled */ + it = pango_attr_list_get_iterator (entry->priv->attr_list); + + if (it == NULL) + return; + do { + gint s, e; + pango_attr_iterator_range (it, &s, &e); + if (s == start) { + GSList *attrs = pango_attr_iterator_get_attrs (it); + g_slist_foreach (attrs, (GFunc) pango_attribute_destroy, NULL); + g_slist_free (attrs); + } + } while (pango_attr_iterator_next (it)); + pango_attr_iterator_destroy (it); + + if (word_misspelled (entry, start, end)) + insert_underline (entry, start, end); +} + +static void +spell_entry_recheck_all (ESpellEntry *entry) +{ + GtkWidget *widget = GTK_WIDGET (entry); + PangoLayout *layout; + gint length, i; + + if (!entry->priv->words) + return; + + /* Remove all existing pango attributes. These will get read as we check */ + pango_attr_list_unref (entry->priv->attr_list); + entry->priv->attr_list = pango_attr_list_new (); + + if (entry->priv->checkers && entry->priv->checking_enabled) { + /* Loop through words */ + for (i = 0; entry->priv->words[i]; i++) { + length = strlen (entry->priv->words[i]); + if (length == 0) + continue; + check_word (entry, entry->priv->word_starts[i], entry->priv->word_ends[i]); + } + + layout = gtk_entry_get_layout (GTK_ENTRY (entry)); + pango_layout_set_attributes (layout, entry->priv->attr_list); + } + + if (gtk_widget_get_realized (widget)) + gtk_widget_queue_draw (widget); +} + +static void +get_word_extents_from_position (ESpellEntry *entry, + gint *start, + gint *end, + guint position) +{ + const gchar *text; + gint i, bytes_pos; + + *start = -1; + *end = -1; + + if (entry->priv->words == NULL) + return; + + text = gtk_entry_get_text (GTK_ENTRY (entry)); + bytes_pos = (gint) (g_utf8_offset_to_pointer (text, position) - text); + + for (i = 0; entry->priv->words[i]; i++) { + if (bytes_pos >= entry->priv->word_starts[i] && + bytes_pos <= entry->priv->word_ends[i]) { + *start = entry->priv->word_starts[i]; + *end = entry->priv->word_ends[i]; + return; + } + } +} + +static void +entry_strsplit_utf8 (GtkEntry *entry, + gchar ***set, + gint **starts, + gint **ends) +{ + PangoLayout *layout; + PangoLogAttr *log_attrs; + const gchar *text; + gint n_attrs, n_strings, i, j; + + layout = gtk_entry_get_layout (GTK_ENTRY (entry)); + text = gtk_entry_get_text (GTK_ENTRY (entry)); + pango_layout_get_log_attrs (layout, &log_attrs, &n_attrs); + + /* Find how many words we have */ + n_strings = 0; + for (i = 0; i < n_attrs; i++) + if (log_attrs[i].is_word_start) + n_strings++; + + *set = g_new0 (gchar *, n_strings + 1); + *starts = g_new0 (gint, n_strings); + *ends = g_new0 (gint, n_strings); + + /* Copy out strings */ + for (i = 0, j = 0; i < n_attrs; i++) { + if (log_attrs[i].is_word_start) { + gint cend, bytes; + gchar *start; + + /* Find the end of this string */ + cend = i; + while (!(log_attrs[cend].is_word_end)) + cend++; + + /* Copy sub-string */ + start = g_utf8_offset_to_pointer (text, i); + bytes = (gint) (g_utf8_offset_to_pointer (text, cend) - start); + (*set)[j] = g_new0 (gchar, bytes + 1); + (*starts)[j] = (gint) (start - text); + (*ends)[j] = (gint) (start - text + bytes); + g_utf8_strncpy ((*set)[j], start, cend - i); + + /* Move on to the next word */ + j++; + } + } + + g_free (log_attrs); +} + +static void +add_to_dictionary (GtkWidget *menuitem, + ESpellEntry *entry) +{ + gchar *word; + gint start, end; + GtkhtmlSpellChecker *checker; + + get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character); + word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end); + + checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker"); + if (checker) + gtkhtml_spell_checker_add_word (checker, word, -1); + + g_free (word); + + if (entry->priv->words) { + g_strfreev (entry->priv->words); + g_free (entry->priv->word_starts); + g_free (entry->priv->word_ends); + } + + entry_strsplit_utf8 (GTK_ENTRY (entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + spell_entry_recheck_all (entry); +} + +static void +ignore_all (GtkWidget *menuitem, + ESpellEntry *entry) +{ + gchar *word; + gint start, end; + GSList *li; + + get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character); + word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end); + + for (li = entry->priv->checkers; li; li = g_slist_next (li)) { + GtkhtmlSpellChecker *checker = li->data; + gtkhtml_spell_checker_add_word_to_session (checker, word, -1); + } + + g_free (word); + + if (entry->priv->words) { + g_strfreev (entry->priv->words); + g_free (entry->priv->word_starts); + g_free (entry->priv->word_ends); + } + entry_strsplit_utf8 (GTK_ENTRY (entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + spell_entry_recheck_all (entry); +} + +static void +replace_word (GtkWidget *menuitem, + ESpellEntry *entry) +{ + gchar *oldword; + const gchar *newword; + gint start, end; + gint cursor; + GtkhtmlSpellChecker *checker; + + get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character); + oldword = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end); + newword = gtk_label_get_text (GTK_LABEL (gtk_bin_get_child (GTK_BIN (menuitem)))); + + cursor = gtk_editable_get_position (GTK_EDITABLE (entry)); + /* is the cursor at the end? If so, restore it there */ + if (g_utf8_strlen (gtk_entry_get_text (GTK_ENTRY (entry)), -1) == cursor) + cursor = -1; + else if (cursor > start && cursor <= end) + cursor = start; + + gtk_editable_delete_text (GTK_EDITABLE (entry), start, end); + gtk_editable_set_position (GTK_EDITABLE (entry), start); + gtk_editable_insert_text ( + GTK_EDITABLE (entry), newword, strlen (newword), + &start); + gtk_editable_set_position (GTK_EDITABLE (entry), cursor); + + checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker"); + + if (checker) + gtkhtml_spell_checker_store_replacement (checker, oldword, -1, newword, -1); + + g_free (oldword); +} + +static void +build_suggestion_menu (ESpellEntry *entry, + GtkWidget *menu, + GtkhtmlSpellChecker *checker, + const gchar *word) +{ + GtkWidget *mi; + GList *suggestions, *iter; + + suggestions = gtkhtml_spell_checker_get_suggestions (checker, word, -1); + + if (!suggestions) { + /* no suggestions. Put something in the menu anyway... */ + GtkWidget *label = gtk_label_new (_("(no suggestions)")); + PangoAttribute *attribute; + PangoAttrList *attribute_list; + + attribute_list = pango_attr_list_new (); + attribute = pango_attr_style_new (PANGO_STYLE_ITALIC); + pango_attr_list_insert (attribute_list, attribute); + gtk_label_set_attributes (GTK_LABEL (label), attribute_list); + pango_attr_list_unref (attribute_list); + + mi = gtk_separator_menu_item_new (); + gtk_container_add (GTK_CONTAINER (mi), label); + gtk_widget_show_all (mi); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi); + } else { + gint ii = 0; + + /* build a set of menus with suggestions */ + for (iter = suggestions; iter; iter = g_list_next (iter), ii++) { + if ((ii != 0) && (ii % 10 == 0)) { + mi = gtk_separator_menu_item_new (); + gtk_widget_show (mi); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi); + + mi = gtk_menu_item_new_with_label (_("More...")); + gtk_widget_show (mi); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi); + + menu = gtk_menu_new (); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu); + } + + mi = gtk_menu_item_new_with_label (iter->data); + g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker); + g_signal_connect (mi, "activate", G_CALLBACK (replace_word), entry); + gtk_widget_show (mi); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi); + } + } + + g_list_free_full (suggestions, g_free); +} + +static GtkWidget * +build_spelling_menu (ESpellEntry *entry, + const gchar *word) +{ + GtkhtmlSpellChecker *checker; + GtkWidget *topmenu, *mi; + gchar *label; + + topmenu = gtk_menu_new (); + + if (!entry->priv->checkers) + return topmenu; + + /* Suggestions */ + if (!entry->priv->checkers->next) { + checker = entry->priv->checkers->data; + build_suggestion_menu (entry, topmenu, checker, word); + } else { + GSList *li; + GtkWidget *menu; + const gchar *lang_name; + + for (li = entry->priv->checkers; li; li = g_slist_next (li)) { + const GtkhtmlSpellLanguage *language; + + checker = li->data; + language = gtkhtml_spell_checker_get_language (checker); + if (!language) + continue; + + lang_name = gtkhtml_spell_language_get_name (language); + if (!lang_name) + lang_name = gtkhtml_spell_language_get_code (language); + + mi = gtk_menu_item_new_with_label (lang_name ? lang_name : "???"); + + gtk_widget_show (mi); + gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi); + menu = gtk_menu_new (); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu); + build_suggestion_menu (entry, menu, checker, word); + } + } + + /* Separator */ + mi = gtk_separator_menu_item_new (); + gtk_widget_show (mi); + gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi); + + /* + Add to Dictionary */ + label = g_strdup_printf (_("Add \"%s\" to Dictionary"), word); + mi = gtk_image_menu_item_new_with_label (label); + g_free (label); + + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_MENU)); + + if (!entry->priv->checkers->next) { + checker = entry->priv->checkers->data; + g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker); + g_signal_connect (mi, "activate", G_CALLBACK (add_to_dictionary), entry); + } else { + GSList *li; + GtkWidget *menu, *submi; + const gchar *lang_name; + + menu = gtk_menu_new (); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu); + + for (li = entry->priv->checkers; li; li = g_slist_next (li)) { + const GtkhtmlSpellLanguage *language; + + checker = li->data; + language = gtkhtml_spell_checker_get_language (checker); + if (!language) + continue; + + lang_name = gtkhtml_spell_language_get_name (language); + if (!lang_name) + lang_name = gtkhtml_spell_language_get_code (language); + + submi = gtk_menu_item_new_with_label (lang_name ? lang_name : "???"); + g_object_set_data (G_OBJECT (submi), "spell-entry-checker", checker); + g_signal_connect (submi, "activate", G_CALLBACK (add_to_dictionary), entry); + + gtk_widget_show (submi); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), submi); + } + } + + gtk_widget_show_all (mi); + gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi); + + /* - Ignore All */ + mi = gtk_image_menu_item_new_with_label (_("Ignore All")); + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), gtk_image_new_from_stock (GTK_STOCK_REMOVE, GTK_ICON_SIZE_MENU)); + g_signal_connect (mi, "activate", G_CALLBACK (ignore_all), entry); + gtk_widget_show_all (mi); + gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi); + + return topmenu; +} + +static void +spell_entry_add_suggestions_menu (ESpellEntry *entry, + GtkMenu *menu, + const gchar *word) +{ + GtkWidget *icon, *mi; + + g_return_if_fail (menu != NULL); + g_return_if_fail (word != NULL); + + /* separator */ + mi = gtk_separator_menu_item_new (); + gtk_widget_show (mi); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi); + + /* Above the separator, show the suggestions menu */ + icon = gtk_image_new_from_stock (GTK_STOCK_SPELL_CHECK, GTK_ICON_SIZE_MENU); + mi = gtk_image_menu_item_new_with_label (_("Spelling Suggestions")); + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), icon); + + gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), build_spelling_menu (entry, word)); + + gtk_widget_show_all (mi); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi); +} + +static gboolean +spell_entry_popup_menu (ESpellEntry *entry) +{ + /* Menu popped up from a keybinding (menu key or <shift>+F10). Use + * the cursor position as the mark position */ + entry->priv->mark_character = gtk_editable_get_position (GTK_EDITABLE (entry)); + + return FALSE; +} + +static void +spell_entry_populate_popup (ESpellEntry *entry, + GtkMenu *menu, + gpointer data) +{ + gint start, end; + gchar *word; + + if (!entry->priv->checkers) + return; + + get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character); + if (start == end) + return; + + if (!word_misspelled (entry, start, end)) + return; + + word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end); + g_return_if_fail (word != NULL); + + spell_entry_add_suggestions_menu (entry, menu, word); + + g_free (word); +} + +static void +spell_entry_changed (GtkEditable *editable) +{ + ESpellEntry *entry = E_SPELL_ENTRY (editable); + + if (!entry->priv->checkers) + return; + + if (entry->priv->words) { + g_strfreev (entry->priv->words); + g_free (entry->priv->word_starts); + g_free (entry->priv->word_ends); + } + entry_strsplit_utf8 (GTK_ENTRY (entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + spell_entry_recheck_all (entry); +} + +static void +spell_entry_notify_scroll_offset (ESpellEntry *spell_entry) +{ + g_return_if_fail (spell_entry != NULL); + + g_object_get (G_OBJECT (spell_entry), "scroll-offset", &spell_entry->priv->entry_scroll_offset, NULL); +} + +static GList * +spell_entry_load_spell_languages (void) +{ + GSettings *settings; + GList *spell_languages = NULL; + gchar **strv; + gint ii; + + /* Ask GSettings for a list of spell check language codes. */ + settings = g_settings_new ("org.gnome.evolution.mail"); + strv = g_settings_get_strv (settings, "composer-spell-languages"); + g_object_unref (settings); + + /* Convert the codes to spell language structs. */ + for (ii = 0; strv[ii] != NULL; ii++) { + gchar *language_code = strv[ii]; + const GtkhtmlSpellLanguage *language; + + language = gtkhtml_spell_language_lookup (language_code); + if (language != NULL) + spell_languages = g_list_prepend ( + spell_languages, (gpointer) language); + } + + g_strfreev (strv); + + spell_languages = g_list_reverse (spell_languages); + + /* Pick a default spell language if it came back empty. */ + if (spell_languages == NULL) { + const GtkhtmlSpellLanguage *language; + + language = gtkhtml_spell_language_lookup (NULL); + + if (language) { + spell_languages = g_list_prepend ( + spell_languages, (gpointer) language); + } + } + + return spell_languages; +} + +static void +spell_entry_settings_changed (ESpellEntry *spell_entry, + const gchar *key) +{ + GList *languages; + + g_return_if_fail (spell_entry != NULL); + + if (spell_entry->priv->custom_checkers) + return; + + if (key && !g_str_equal (key, "composer-spell-languages")) + return; + + languages = spell_entry_load_spell_languages (); + e_spell_entry_set_languages (spell_entry, languages); + g_list_free (languages); + + spell_entry->priv->custom_checkers = FALSE; +} + +static gint +spell_entry_find_position (ESpellEntry *spell_entry, + gint x) +{ + PangoLayout *layout; + PangoLayoutLine *line; + gint index; + gint pos; + gint trailing; + const gchar *text; + GtkEntry *entry = GTK_ENTRY (spell_entry); + + layout = gtk_entry_get_layout (entry); + text = pango_layout_get_text (layout); + + line = pango_layout_get_lines_readonly (layout)->data; + pango_layout_line_x_to_index (line, x * PANGO_SCALE, &index, &trailing); + + pos = g_utf8_pointer_to_offset (text, text + index); + pos += trailing; + + return pos; +} + +static gboolean +e_spell_entry_draw (GtkWidget *widget, + cairo_t *cr) +{ + ESpellEntry *spell_entry = E_SPELL_ENTRY (widget); + GtkEntry *entry = GTK_ENTRY (widget); + PangoLayout *layout; + + layout = gtk_entry_get_layout (entry); + pango_layout_set_attributes (layout, spell_entry->priv->attr_list); + + return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->draw (widget, cr); +} + +static gboolean +e_spell_entry_button_press (GtkWidget *widget, + GdkEventButton *event) +{ + ESpellEntry *spell_entry = E_SPELL_ENTRY (widget); + + spell_entry->priv->mark_character = spell_entry_find_position ( + spell_entry, event->x + spell_entry->priv->entry_scroll_offset); + + return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->button_press_event (widget, event); +} + +static void +spell_entry_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CHECKING_ENABLED: + e_spell_entry_set_checking_enabled ( + E_SPELL_ENTRY (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +spell_entry_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CHECKING_ENABLED: + g_value_set_boolean ( + value, + e_spell_entry_get_checking_enabled ( + E_SPELL_ENTRY (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +e_spell_entry_init (ESpellEntry *spell_entry) +{ + spell_entry->priv = E_SPELL_ENTRY_GET_PRIVATE (spell_entry); + spell_entry->priv->attr_list = pango_attr_list_new (); + spell_entry->priv->checkers = NULL; + spell_entry->priv->checking_enabled = TRUE; + + g_signal_connect (spell_entry, "popup-menu", G_CALLBACK (spell_entry_popup_menu), NULL); + g_signal_connect (spell_entry, "populate-popup", G_CALLBACK (spell_entry_populate_popup), NULL); + g_signal_connect (spell_entry, "changed", G_CALLBACK (spell_entry_changed), NULL); + g_signal_connect (spell_entry, "notify::scroll-offset", G_CALLBACK (spell_entry_notify_scroll_offset), NULL); + + /* listen for languages changes */ + spell_entry->priv->settings = g_settings_new ("org.gnome.evolution.mail"); + g_signal_connect_swapped (spell_entry->priv->settings, "changed", G_CALLBACK (spell_entry_settings_changed), spell_entry); + + /* load current settings */ + spell_entry_settings_changed (spell_entry, NULL); +} + +static void +e_spell_entry_finalize (GObject *object) +{ + ESpellEntry *entry; + + g_return_if_fail (object != NULL); + g_return_if_fail (E_IS_SPELL_ENTRY (object)); + + entry = E_SPELL_ENTRY (object); + + if (entry->priv->settings) + g_object_unref (entry->priv->settings); + if (entry->priv->checkers) + g_slist_free_full (entry->priv->checkers, g_object_unref); + if (entry->priv->attr_list) + pango_attr_list_unref (entry->priv->attr_list); + if (entry->priv->words) + g_strfreev (entry->priv->words); + if (entry->priv->word_starts) + g_free (entry->priv->word_starts); + if (entry->priv->word_ends) + g_free (entry->priv->word_ends); + + G_OBJECT_CLASS (e_spell_entry_parent_class)->finalize (object); +} + +static void +e_spell_entry_class_init (ESpellEntryClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (ESpellEntryPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = spell_entry_set_property; + object_class->get_property = spell_entry_get_property; + object_class->finalize = e_spell_entry_finalize; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->draw = e_spell_entry_draw; + widget_class->button_press_event = e_spell_entry_button_press; + + g_object_class_install_property ( + object_class, + PROP_CHECKING_ENABLED, + g_param_spec_boolean ( + "checking-enabled", + "checking enabled", + "Spell Checking is Enabled", + TRUE, + G_PARAM_READWRITE)); +} + +GtkWidget * +e_spell_entry_new (void) +{ + return g_object_new (E_TYPE_SPELL_ENTRY, NULL); +} + +/* 'languages' consists of 'const GtkhtmlSpellLanguage *' */ +void +e_spell_entry_set_languages (ESpellEntry *spell_entry, + GList *languages) +{ + GList *iter; + + g_return_if_fail (spell_entry != NULL); + + spell_entry->priv->custom_checkers = TRUE; + + if (spell_entry->priv->checkers) + g_slist_free_full (spell_entry->priv->checkers, g_object_unref); + spell_entry->priv->checkers = NULL; + + for (iter = languages; iter; iter = g_list_next (iter)) { + const GtkhtmlSpellLanguage *language = iter->data; + + if (language) + spell_entry->priv->checkers = g_slist_prepend ( + spell_entry->priv->checkers, + gtkhtml_spell_checker_new (language)); + } + + spell_entry->priv->checkers = g_slist_reverse (spell_entry->priv->checkers); + + if (gtk_widget_get_realized (GTK_WIDGET (spell_entry))) + spell_entry_recheck_all (spell_entry); +} + +gboolean +e_spell_entry_get_checking_enabled (ESpellEntry *spell_entry) +{ + g_return_val_if_fail (spell_entry != NULL, FALSE); + + return spell_entry->priv->checking_enabled; +} + +void +e_spell_entry_set_checking_enabled (ESpellEntry *spell_entry, + gboolean enable_checking) +{ + g_return_if_fail (spell_entry != NULL); + + if (spell_entry->priv->checking_enabled == enable_checking) + return; + + spell_entry->priv->checking_enabled = enable_checking; + spell_entry_recheck_all (spell_entry); + + g_object_notify (G_OBJECT (spell_entry), "checking-enabled"); + +} diff --git a/e-util/e-spell-entry.h b/e-util/e-spell-entry.h new file mode 100644 index 0000000000..07c4c0d24d --- /dev/null +++ b/e-util/e-spell-entry.h @@ -0,0 +1,63 @@ +/* + * e-spell-entry.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SPELL_ENTRY_H +#define E_SPELL_ENTRY_H + +#include <gtk/gtk.h> + +#define E_TYPE_SPELL_ENTRY (e_spell_entry_get_type()) +#define E_SPELL_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), E_TYPE_SPELL_ENTRY, ESpellEntry)) +#define E_SPELL_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), E_TYPE_SPELL_ENTRY, ESpellEntryClass)) +#define E_IS_SPELL_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), E_TYPE_SPELL_ENTRY)) +#define E_IS_SPELL_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), E_TYPE_SPELL_ENTRY)) +#define E_SPELL_ENTRY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), E_TYPE_SPELL_ENTRY, ESpellEntryClass)) + +G_BEGIN_DECLS + +typedef struct _ESpellEntry ESpellEntry; +typedef struct _ESpellEntryClass ESpellEntryClass; +typedef struct _ESpellEntryPrivate ESpellEntryPrivate; + +struct _ESpellEntry +{ + GtkEntry parent_object; + + ESpellEntryPrivate *priv; +}; + +struct _ESpellEntryClass +{ + GtkEntryClass parent_class; +}; + +GType e_spell_entry_get_type (void); +GtkWidget * e_spell_entry_new (void); +void e_spell_entry_set_languages (ESpellEntry *spell_entry, + GList *languages); +gboolean e_spell_entry_get_checking_enabled (ESpellEntry *spell_entry); +void e_spell_entry_set_checking_enabled (ESpellEntry *spell_entry, + gboolean enable_checking); + +G_END_DECLS + +#endif /* E_SPELL_ENTRY_H */ diff --git a/e-util/e-stock-request.c b/e-util/e-stock-request.c index 8736dba7d4..2b00f9faa4 100644 --- a/e-util/e-stock-request.c +++ b/e-util/e-stock-request.c @@ -21,10 +21,9 @@ #include "e-stock-request.h" #include <stdlib.h> +#include <gtk/gtk.h> #include <libsoup/soup.h> -#include <e-util/e-util.h> - #include <string.h> #define d(x) diff --git a/e-util/e-stock-request.h b/e-util/e-stock-request.h index 39a22ba424..b482d7fada 100644 --- a/e-util/e-stock-request.h +++ b/e-util/e-stock-request.h @@ -16,6 +16,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_STOCK_REQUEST_H #define E_STOCK_REQUEST_H diff --git a/e-util/e-table-click-to-add.c b/e-util/e-table-click-to-add.c new file mode 100644 index 0000000000..6de00f913b --- /dev/null +++ b/e-util/e-table-click-to-add.c @@ -0,0 +1,666 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-click-to-add.h" + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-canvas-utils.h" +#include "e-canvas.h" +#include "e-marshal.h" +#include "e-table-defines.h" +#include "e-table-header.h" +#include "e-table-one.h" +#include "e-text.h" +#include "gal-a11y-e-table-click-to-add.h" + +enum { + CURSOR_CHANGE, + STYLE_SET, + LAST_SIGNAL +}; + +static guint etcta_signals[LAST_SIGNAL] = { 0 }; + +/* workaround for avoiding APi breakage */ +#define etcta_get_type e_table_click_to_add_get_type +G_DEFINE_TYPE (ETableClickToAdd, etcta, GNOME_TYPE_CANVAS_GROUP) + +enum { + PROP_0, + PROP_HEADER, + PROP_MODEL, + PROP_MESSAGE, + PROP_WIDTH, + PROP_HEIGHT +}; + +static void +etcta_cursor_change (GObject *object, + gint row, + gint col, + ETableClickToAdd *etcta) +{ + g_signal_emit ( + etcta, + etcta_signals[CURSOR_CHANGE], 0, + row, col); +} + +static void +etcta_style_set (ETableClickToAdd *etcta, + GtkStyle *previous_style) +{ + GtkWidget *widget; + GtkStyle *style; + + widget = GTK_WIDGET (GNOME_CANVAS_ITEM (etcta)->canvas); + style = gtk_widget_get_style (widget); + + if (etcta->rect) + gnome_canvas_item_set ( + etcta->rect, + "outline_color_gdk", &style->fg[GTK_STATE_NORMAL], + "fill_color_gdk", &style->bg[GTK_STATE_NORMAL], + NULL); + + if (etcta->text) + gnome_canvas_item_set ( + etcta->text, + "fill_color_gdk", &style->text[GTK_STATE_NORMAL], + NULL); +} + +static void +etcta_add_table_header (ETableClickToAdd *etcta, + ETableHeader *header) +{ + etcta->eth = header; + if (etcta->eth) + g_object_ref (etcta->eth); + if (etcta->row) + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etcta->row), + "ETableHeader", header, + NULL); +} + +static void +etcta_drop_table_header (ETableClickToAdd *etcta) +{ + if (!etcta->eth) + return; + + g_object_unref (etcta->eth); + etcta->eth = NULL; +} + +static void +etcta_add_one (ETableClickToAdd *etcta, + ETableModel *one) +{ + etcta->one = one; + if (etcta->one) + g_object_ref (etcta->one); + if (etcta->row) + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etcta->row), + "ETableModel", one, + NULL); + g_object_set ( + etcta->selection, + "model", one, + NULL); +} + +static void +etcta_drop_one (ETableClickToAdd *etcta) +{ + if (!etcta->one) + return; + g_object_unref (etcta->one); + etcta->one = NULL; + g_object_set ( + etcta->selection, + "model", NULL, + NULL); +} + +static void +etcta_add_model (ETableClickToAdd *etcta, + ETableModel *model) +{ + etcta->model = model; + if (etcta->model) + g_object_ref (etcta->model); +} + +static void +etcta_drop_model (ETableClickToAdd *etcta) +{ + etcta_drop_one (etcta); + if (!etcta->model) + return; + g_object_unref (etcta->model); + etcta->model = NULL; +} + +static void +etcta_add_message (ETableClickToAdd *etcta, + const gchar *message) +{ + etcta->message = g_strdup (message); +} + +static void +etcta_drop_message (ETableClickToAdd *etcta) +{ + g_free (etcta->message); + etcta->message = NULL; +} + +static void +etcta_dispose (GObject *object) +{ + ETableClickToAdd *etcta = E_TABLE_CLICK_TO_ADD (object); + + etcta_drop_table_header (etcta); + etcta_drop_model (etcta); + etcta_drop_message (etcta); + if (etcta->selection) + g_object_unref (etcta->selection); + etcta->selection = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (etcta_parent_class)->dispose (object); +} + +static void +etcta_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GnomeCanvasItem *item; + ETableClickToAdd *etcta; + + item = GNOME_CANVAS_ITEM (object); + etcta = E_TABLE_CLICK_TO_ADD (object); + + switch (property_id) { + case PROP_HEADER: + etcta_drop_table_header (etcta); + etcta_add_table_header (etcta, E_TABLE_HEADER (g_value_get_object (value))); + break; + case PROP_MODEL: + etcta_drop_model (etcta); + etcta_add_model (etcta, E_TABLE_MODEL (g_value_get_object (value))); + break; + case PROP_MESSAGE: + etcta_drop_message (etcta); + etcta_add_message (etcta, g_value_get_string (value)); + break; + case PROP_WIDTH: + etcta->width = g_value_get_double (value); + if (etcta->row) + gnome_canvas_item_set ( + etcta->row, + "minimum_width", etcta->width, + NULL); + if (etcta->text) + gnome_canvas_item_set ( + etcta->text, + "width", (etcta->width < 4 ? 4 : etcta->width) - 4, + NULL); + if (etcta->rect) + gnome_canvas_item_set ( + etcta->rect, + "x2", etcta->width - 1, + NULL); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + return; + + } + gnome_canvas_item_request_update (item); +} + +static void +create_rect_and_text (ETableClickToAdd *etcta) +{ + GtkWidget *widget; + GtkStyle *style; + + widget = GTK_WIDGET (GNOME_CANVAS_ITEM (etcta)->canvas); + style = gtk_widget_get_style (widget); + + if (!etcta->rect) + etcta->rect = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (etcta), + gnome_canvas_rect_get_type (), + "x1", (gdouble) 0, + "y1", (gdouble) 0, + "x2", (gdouble) etcta->width - 1, + "y2", (gdouble) etcta->height - 1, + "outline_color_gdk", &style->fg[GTK_STATE_NORMAL], + "fill_color_gdk", &style->bg[GTK_STATE_NORMAL], + NULL); + + if (!etcta->text) + etcta->text = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (etcta), + e_text_get_type (), + "text", etcta->message ? etcta->message : "", + "width", etcta->width - 4, + "fill_color_gdk", &style->text[GTK_STATE_NORMAL], + NULL); +} + +static void +etcta_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableClickToAdd *etcta; + + etcta = E_TABLE_CLICK_TO_ADD (object); + + switch (property_id) { + case PROP_HEADER: + g_value_set_object (value, etcta->eth); + break; + case PROP_MODEL: + g_value_set_object (value, etcta->model); + break; + case PROP_MESSAGE: + g_value_set_string (value, etcta->message); + break; + case PROP_WIDTH: + g_value_set_double (value, etcta->width); + break; + case PROP_HEIGHT: + g_value_set_double (value, etcta->height); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +etcta_realize (GnomeCanvasItem *item) +{ + ETableClickToAdd *etcta = E_TABLE_CLICK_TO_ADD (item); + + create_rect_and_text (etcta); + e_canvas_item_move_absolute (etcta->text, 2, 2); + + if (GNOME_CANVAS_ITEM_CLASS (etcta_parent_class)->realize) + (*GNOME_CANVAS_ITEM_CLASS (etcta_parent_class)->realize)(item); + + e_canvas_item_request_reflow (item); +} + +static void +etcta_unrealize (GnomeCanvasItem *item) +{ + if (GNOME_CANVAS_ITEM_CLASS (etcta_parent_class)->unrealize) + (*GNOME_CANVAS_ITEM_CLASS (etcta_parent_class)->unrealize)(item); +} + +static void finish_editing (ETableClickToAdd *etcta); + +static gint +item_key_press (ETableItem *item, + gint row, + gint col, + GdkEvent *event, + ETableClickToAdd *etcta) +{ + switch (event->key.keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + case GDK_KEY_ISO_Enter: + case GDK_KEY_3270_Enter: + finish_editing (etcta); + return TRUE; + } + return FALSE; +} + +static void +set_initial_selection (ETableClickToAdd *etcta) +{ + e_selection_model_do_something ( + E_SELECTION_MODEL (etcta->selection), + 0, e_table_header_prioritized_column (etcta->eth), + 0); +} + +static void +finish_editing (ETableClickToAdd *etcta) +{ + if (etcta->row) { + ETableModel *one; + + e_table_item_leave_edit (E_TABLE_ITEM (etcta->row)); + e_table_one_commit (E_TABLE_ONE (etcta->one)); + etcta_drop_one (etcta); + g_object_run_dispose (G_OBJECT (etcta->row)); + etcta->row = NULL; + + one = e_table_one_new (etcta->model); + etcta_add_one (etcta, one); + g_object_unref (one); + + e_selection_model_clear (E_SELECTION_MODEL (etcta->selection)); + + etcta->row = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (etcta), + e_table_item_get_type (), + "ETableHeader", etcta->eth, + "ETableModel", etcta->one, + "minimum_width", etcta->width, + "horizontal_draw_grid", TRUE, + "vertical_draw_grid", TRUE, + "selection_model", etcta->selection, + "cursor_mode", E_CURSOR_SPREADSHEET, + NULL); + + g_signal_connect ( + etcta->row, "key_press", + G_CALLBACK (item_key_press), etcta); + + set_initial_selection (etcta); + } +} + +/* Handles the events on the ETableClickToAdd, particularly + * it creates the ETableItem and passes in some events. */ +static gint +etcta_event (GnomeCanvasItem *item, + GdkEvent *e) +{ + ETableClickToAdd *etcta = E_TABLE_CLICK_TO_ADD (item); + + switch (e->type) { + case GDK_FOCUS_CHANGE: + if (!e->focus_change.in) + return TRUE; + + case GDK_BUTTON_PRESS: + if (etcta->text) { + g_object_run_dispose (G_OBJECT (etcta->text)); + etcta->text = NULL; + } + if (etcta->rect) { + g_object_run_dispose (G_OBJECT (etcta->rect)); + etcta->rect = NULL; + } + if (!etcta->row) { + ETableModel *one; + + one = e_table_one_new (etcta->model); + etcta_add_one (etcta, one); + g_object_unref (one); + + e_selection_model_clear (E_SELECTION_MODEL (etcta->selection)); + + etcta->row = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (item), + e_table_item_get_type (), + "ETableHeader", etcta->eth, + "ETableModel", etcta->one, + "minimum_width", etcta->width, + "horizontal_draw_grid", TRUE, + "vertical_draw_grid", TRUE, + "selection_model", etcta->selection, + "cursor_mode", E_CURSOR_SPREADSHEET, + NULL); + + g_signal_connect ( + etcta->row, "key_press", + G_CALLBACK (item_key_press), etcta); + + e_canvas_item_grab_focus (GNOME_CANVAS_ITEM (etcta->row), TRUE); + + set_initial_selection (etcta); + } + break; + + case GDK_KEY_PRESS: + switch (e->key.keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + case GDK_KEY_ISO_Left_Tab: + finish_editing (etcta); + break; + default: + return FALSE; + case GDK_KEY_Escape: + if (etcta->row) { + e_table_item_leave_edit (E_TABLE_ITEM (etcta->row)); + etcta_drop_one (etcta); + g_object_run_dispose (G_OBJECT (etcta->row)); + etcta->row = NULL; + create_rect_and_text (etcta); + e_canvas_item_move_absolute (etcta->text, 3, 3); + } + break; + } + break; + + default: + return FALSE; + } + return TRUE; +} + +static void +etcta_reflow (GnomeCanvasItem *item, + gint flags) +{ + ETableClickToAdd *etcta = E_TABLE_CLICK_TO_ADD (item); + + gdouble old_height = etcta->height; + + if (etcta->text) { + g_object_get ( + etcta->text, + "height", &etcta->height, + NULL); + etcta->height += 6; + } + if (etcta->row) { + g_object_get ( + etcta->row, + "height", &etcta->height, + NULL); + } + + if (etcta->rect) { + g_object_set ( + etcta->rect, + "y2", etcta->height - 1, + NULL); + } + + if (old_height != etcta->height) + e_canvas_item_request_parent_reflow (item); +} + +static void +etcta_class_init (ETableClickToAddClass *class) +{ + GnomeCanvasItemClass *item_class = GNOME_CANVAS_ITEM_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + class->cursor_change = NULL; + class->style_set = etcta_style_set; + + object_class->dispose = etcta_dispose; + object_class->set_property = etcta_set_property; + object_class->get_property = etcta_get_property; + + item_class->realize = etcta_realize; + item_class->unrealize = etcta_unrealize; + item_class->event = etcta_event; + + g_object_class_install_property ( + object_class, + PROP_HEADER, + g_param_spec_object ( + "header", + "Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MODEL, + g_param_spec_object ( + "model", + "Model", + NULL, + E_TYPE_TABLE_MODEL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MESSAGE, + g_param_spec_string ( + "message", + "Message", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WIDTH, + g_param_spec_double ( + "width", + "Width", + NULL, + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE | + G_PARAM_LAX_VALIDATION)); + + g_object_class_install_property ( + object_class, + PROP_HEIGHT, + g_param_spec_double ( + "height", + "Height", + NULL, + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READABLE | + G_PARAM_LAX_VALIDATION)); + + etcta_signals[CURSOR_CHANGE] = g_signal_new ( + "cursor_change", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClickToAddClass, cursor_change), + NULL, NULL, + e_marshal_VOID__INT_INT, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_INT); + + etcta_signals[STYLE_SET] = g_signal_new ( + "style_set", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClickToAddClass, style_set), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + GTK_TYPE_STYLE); + + gal_a11y_e_table_click_to_add_init (); +} + +static void +etcta_init (ETableClickToAdd *etcta) +{ + AtkObject *a11y; + + etcta->one = NULL; + etcta->model = NULL; + etcta->eth = NULL; + + etcta->message = NULL; + + etcta->row = NULL; + etcta->text = NULL; + etcta->rect = NULL; + + /* Pick some arbitrary defaults. */ + etcta->width = 12; + etcta->height = 6; + + etcta->selection = e_table_selection_model_new (); + g_signal_connect ( + etcta->selection, "cursor_changed", + G_CALLBACK (etcta_cursor_change), etcta); + + e_canvas_item_set_reflow_callback (GNOME_CANVAS_ITEM (etcta), etcta_reflow); + + /* create its a11y object at this time if accessibility is enabled*/ + if (atk_get_root () != NULL) { + a11y = atk_gobject_accessible_for_object (G_OBJECT (etcta)); + atk_object_set_name (a11y, _("click to add")); + } +} + +/* The colors in this need to be themefied. */ +/** + * e_table_click_to_add_commit: + * @etcta: The %ETableClickToAdd to commit. + * + * This routine commits the current thing being edited and returns to + * just displaying the click to add message. + **/ +void +e_table_click_to_add_commit (ETableClickToAdd *etcta) +{ + if (etcta->row) { + e_table_one_commit (E_TABLE_ONE (etcta->one)); + etcta_drop_one (etcta); + g_object_run_dispose (G_OBJECT (etcta->row)); + etcta->row = NULL; + } + create_rect_and_text (etcta); + e_canvas_item_move_absolute (etcta->text, 3, 3); +} diff --git a/e-util/e-table-click-to-add.h b/e-util/e-table-click-to-add.h new file mode 100644 index 0000000000..cd1519b82e --- /dev/null +++ b/e-util/e-table-click-to-add.h @@ -0,0 +1,99 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_CLICK_TO_ADD_H_ +#define _E_TABLE_CLICK_TO_ADD_H_ + +#include <libxml/tree.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-table-header.h> +#include <e-util/e-table-item.h> +#include <e-util/e-table-selection-model.h> +#include <e-util/e-table-sort-info.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_CLICK_TO_ADD \ + (e_table_click_to_add_get_type ()) +#define E_TABLE_CLICK_TO_ADD(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_CLICK_TO_ADD, ETableClickToAdd)) +#define E_TABLE_CLICK_TO_ADD_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_CLICK_TO_ADD, ETableClickToAddClass)) +#define E_IS_TABLE_CLICK_TO_ADD(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_CLICK_TO_ADD)) +#define E_IS_TABLE_CLICK_TO_ADD_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_CLICK_TO_ADD)) +#define E_TABLE_CLICK_TO_ADD_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_CLICK_TO_ADD, ETableClickToAddClass)) + +G_BEGIN_DECLS + +typedef struct _ETableClickToAdd ETableClickToAdd; +typedef struct _ETableClickToAddClass ETableClickToAddClass; + +struct _ETableClickToAdd { + GnomeCanvasGroup parent; + + ETableModel *one; /* The ETableOne. */ + + ETableModel *model; /* The backend model. */ + ETableHeader *eth; /* This is just to give to the ETableItem. */ + + gchar *message; + + GnomeCanvasItem *row; /* If row is NULL, we're sitting with + * no data and a "Click here" message. */ + GnomeCanvasItem *text; /* If text is NULL, row shouldn't be. */ + GnomeCanvasItem *rect; /* What the heck. Why not. */ + + gdouble width; + gdouble height; + + ETableSelectionModel *selection; +}; + +struct _ETableClickToAddClass { + GnomeCanvasGroupClass parent_class; + + /* Signals */ + void (*cursor_change) (ETableClickToAdd *etcta, + gint row, + gint col); + void (*style_set) (ETableClickToAdd *etcta, + GtkStyle *previous_style); +}; + +GType e_table_click_to_add_get_type (void) G_GNUC_CONST; +void e_table_click_to_add_commit (ETableClickToAdd *etcta); + +G_END_DECLS + +#endif /* _E_TABLE_CLICK_TO_ADD_H_ */ diff --git a/e-util/e-table-col-dnd.h b/e-util/e-table-col-dnd.h new file mode 100644 index 0000000000..608e14e826 --- /dev/null +++ b/e-util/e-table-col-dnd.h @@ -0,0 +1,43 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_COL_DND_H_ +#define _E_TABLE_COL_DND_H_ + +#include <glib.h> + +G_BEGIN_DECLS + +#define TARGET_ETABLE_COL_TYPE "application/x-etable-column-header" + +enum { + TARGET_ETABLE_COL_HEADER +}; + +G_END_DECLS + +#endif /* _E_TABLE_COL_DND_H_ */ diff --git a/e-util/e-table-col.c b/e-util/e-table-col.c new file mode 100644 index 0000000000..4e5e18a5b6 --- /dev/null +++ b/e-util/e-table-col.c @@ -0,0 +1,222 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <glib/gi18n.h> + +#include "e-table-col.h" + +G_DEFINE_TYPE (ETableCol, e_table_col, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_COMPARE_COL +}; + +static void +etc_load_icon (ETableCol *etc) +{ + /* FIXME This ignores theme changes. */ + + GtkIconTheme *icon_theme; + gint width, height; + GError *error = NULL; + + icon_theme = gtk_icon_theme_get_default (); + gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, &height); + + etc->pixbuf = gtk_icon_theme_load_icon ( + icon_theme, etc->icon_name, height, 0, &error); + + if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } +} + +static void +etc_dispose (GObject *object) +{ + ETableCol *etc = E_TABLE_COL (object); + + if (etc->ecell) + g_object_unref (etc->ecell); + etc->ecell = NULL; + + if (etc->pixbuf) + g_object_unref (etc->pixbuf); + etc->pixbuf = NULL; + + g_free (etc->text); + etc->text = NULL; + + g_free (etc->icon_name); + etc->icon_name = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_table_col_parent_class)->dispose (object); +} + +static void +etc_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ETableCol *etc = E_TABLE_COL (object); + + switch (property_id) { + case PROP_COMPARE_COL: + etc->compare_col = g_value_get_int (value); + break; + default: + break; + } +} + +static void +etc_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableCol *etc = E_TABLE_COL (object); + + switch (property_id) { + case PROP_COMPARE_COL: + g_value_set_int (value, etc->compare_col); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +e_table_col_class_init (ETableColClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = etc_dispose; + object_class->set_property = etc_set_property; + object_class->get_property = etc_get_property; + + g_object_class_install_property ( + object_class, + PROP_COMPARE_COL, + g_param_spec_int ( + "compare_col", + "Width", + "Width", + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); +} + +static void +e_table_col_init (ETableCol *etc) +{ + etc->width = 0; + etc->sortable = 1; + etc->groupable = 1; + etc->justification = GTK_JUSTIFY_LEFT; + etc->priority = 0; +} + +/** + * e_table_col_new: + * @col_idx: the column we represent in the model + * @text: a title for this column + * @icon_name: name of the icon to be used for the header, or %NULL + * @expansion: FIXME + * @min_width: minimum width in pixels for this column + * @ecell: the renderer to be used for this column + * @compare: comparision function for the elements stored in this column + * @resizable: whether the column can be resized interactively by the user + * @priority: FIXME + * + * The ETableCol represents a column to be used inside an ETable. The + * ETableCol objects are inserted inside an ETableHeader (which is just a + * collection of ETableCols). The ETableHeader is the definition of the + * order in which columns are shown to the user. + * + * The @text argument is the the text that will be shown as a header to the + * user. @col_idx reflects where the data for this ETableCol object will + * be fetch from an ETableModel. So even if the user changes the order + * of the columns being viewed (the ETableCols in the ETableHeader), the + * column will always point to the same column inside the ETableModel. + * + * The @ecell argument is an ECell object that needs to know how to + * render the data in the ETableModel for this specific row. + * + * Data passed to @compare can be (if not %NULL) a cmp_cache, which + * can be accessed by e_table_sorting_utils_add_to_cmp_cache() and + * e_table_sorting_utils_lookup_cmp_cache(). + * + * Returns: the newly created ETableCol object. + */ +ETableCol * +e_table_col_new (gint col_idx, + const gchar *text, + const gchar *icon_name, + gdouble expansion, + gint min_width, + ECell *ecell, + GCompareDataFunc compare, + gboolean resizable, + gboolean disabled, + gint priority) +{ + ETableCol *etc; + + g_return_val_if_fail (expansion >= 0, NULL); + g_return_val_if_fail (min_width >= 0, NULL); + g_return_val_if_fail (ecell != NULL, NULL); + g_return_val_if_fail (compare != NULL, NULL); + g_return_val_if_fail (text != NULL, NULL); + + etc = g_object_new (E_TYPE_TABLE_COL, NULL); + + etc->col_idx = col_idx; + etc->compare_col = col_idx; + etc->text = g_strdup (text); + etc->icon_name = g_strdup (icon_name); + etc->pixbuf = NULL; + etc->expansion = expansion; + etc->min_width = min_width; + etc->ecell = ecell; + etc->compare = compare; + etc->disabled = disabled; + etc->priority = priority; + + etc->selected = 0; + etc->resizable = resizable; + + g_object_ref (etc->ecell); + + if (etc->icon_name != NULL) + etc_load_icon (etc); + + return etc; +} diff --git a/e-util/e-table-col.h b/e-util/e-table-col.h new file mode 100644 index 0000000000..243aa04e7a --- /dev/null +++ b/e-util/e-table-col.h @@ -0,0 +1,115 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_TABLE_COL_H +#define E_TABLE_COL_H + +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include <e-util/e-cell.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_COL \ + (e_table_col_get_type ()) +#define E_TABLE_COL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_COL, ETableCol)) +#define E_TABLE_COL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_COL, ETableColClass)) +#define E_IS_TABLE_COL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_COL)) +#define E_IS_TABLE_COL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_COL)) +#define E_TABLE_COL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_COL, ETableColClass)) + +G_BEGIN_DECLS + +typedef enum { + E_TABLE_COL_ARROW_NONE = 0, + E_TABLE_COL_ARROW_UP, + E_TABLE_COL_ARROW_DOWN +} ETableColArrow; + +typedef struct _ETableCol ETableCol; +typedef struct _ETableColClass ETableColClass; + +/* + * Information about a single column + */ +struct _ETableCol { + GObject parent; + + gchar *text; + gchar *icon_name; + GdkPixbuf *pixbuf; + gint min_width; + gint width; + gdouble expansion; + gshort x; + GCompareDataFunc compare; + ETableSearchFunc search; + + guint selected : 1; + guint resizable : 1; + guint disabled : 1; + guint sortable : 1; + guint groupable : 1; + + gint col_idx; + gint compare_col; + gint priority; + + GtkJustification justification; + + ECell *ecell; +}; + +struct _ETableColClass { + GObjectClass parent_class; +}; + +GType e_table_col_get_type (void) G_GNUC_CONST; +ETableCol * e_table_col_new (gint col_idx, + const gchar *text, + const gchar *icon_name, + gdouble expansion, + gint min_width, + ECell *ecell, + GCompareDataFunc compare, + gboolean resizable, + gboolean disabled, + gint priority); + +G_END_DECLS + +#endif /* E_TABLE_COL_H */ + diff --git a/e-util/e-table-column-specification.c b/e-util/e-table-column-specification.c new file mode 100644 index 0000000000..d1cf089d2d --- /dev/null +++ b/e-util/e-table-column-specification.c @@ -0,0 +1,157 @@ +/* + * e-table-column-specification.c - Savable specification of a column. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-column-specification.h" + +#include <stdlib.h> + +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include "e-xml-utils.h" + +/* workaround for avoiding API breakage */ +#define etcs_get_type e_table_column_specification_get_type +G_DEFINE_TYPE (ETableColumnSpecification, etcs, G_TYPE_OBJECT) + +static void +free_strings (ETableColumnSpecification *etcs) +{ + g_free (etcs->title); + etcs->title = NULL; + g_free (etcs->pixbuf); + etcs->pixbuf = NULL; + g_free (etcs->cell); + etcs->cell = NULL; + g_free (etcs->compare); + etcs->compare = NULL; + g_free (etcs->search); + etcs->search = NULL; + g_free (etcs->sortable); + etcs->sortable = NULL; +} + +static void +etcs_finalize (GObject *object) +{ + ETableColumnSpecification *etcs = E_TABLE_COLUMN_SPECIFICATION (object); + + free_strings (etcs); + + G_OBJECT_CLASS (etcs_parent_class)->finalize (object); +} + +static void +etcs_class_init (ETableColumnSpecificationClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = etcs_finalize; +} + +static void +etcs_init (ETableColumnSpecification *specification) +{ + specification->model_col = 0; + specification->compare_col = 0; + specification->title = g_strdup (""); + specification->pixbuf = NULL; + + specification->expansion = 0; + specification->minimum_width = 0; + specification->resizable = FALSE; + specification->disabled = FALSE; + + specification->cell = NULL; + specification->compare = NULL; + specification->search = NULL; + specification->priority = 0; +} + +ETableColumnSpecification * +e_table_column_specification_new (void) +{ + return g_object_new (E_TYPE_TABLE_COLUMN_SPECIFICATION, NULL); +} + +void +e_table_column_specification_load_from_node (ETableColumnSpecification *etcs, + const xmlNode *node) +{ + free_strings (etcs); + + etcs->model_col = e_xml_get_integer_prop_by_name (node, (const guchar *)"model_col"); + etcs->compare_col = e_xml_get_integer_prop_by_name_with_default (node, (const guchar *)"compare_col", etcs->model_col); + etcs->title = e_xml_get_string_prop_by_name (node, (const guchar *)"_title"); + etcs->pixbuf = e_xml_get_string_prop_by_name (node, (const guchar *)"pixbuf"); + + etcs->expansion = e_xml_get_double_prop_by_name (node, (const guchar *)"expansion"); + etcs->minimum_width = e_xml_get_integer_prop_by_name (node, (const guchar *)"minimum_width"); + etcs->resizable = e_xml_get_bool_prop_by_name (node, (const guchar *)"resizable"); + etcs->disabled = e_xml_get_bool_prop_by_name (node, (const guchar *)"disabled"); + + etcs->cell = e_xml_get_string_prop_by_name (node, (const guchar *)"cell"); + etcs->compare = e_xml_get_string_prop_by_name (node, (const guchar *)"compare"); + etcs->search = e_xml_get_string_prop_by_name (node, (const guchar *)"search"); + etcs->sortable = e_xml_get_string_prop_by_name (node, (const guchar *)"sortable"); + etcs->priority = e_xml_get_integer_prop_by_name_with_default (node, (const guchar *)"priority", 0); + + if (etcs->title == NULL) + etcs->title = g_strdup (""); +} + +xmlNode * +e_table_column_specification_save_to_node (ETableColumnSpecification *specification, + xmlNode *parent) +{ + xmlNode *node; + if (parent) + node = xmlNewChild (parent, NULL, (const guchar *)"ETableColumn", NULL); + else + node = xmlNewNode (NULL, (const guchar *)"ETableColumn"); + + e_xml_set_integer_prop_by_name (node, (const guchar *)"model_col", specification->model_col); + if (specification->compare_col != specification->model_col) + e_xml_set_integer_prop_by_name (node, (const guchar *)"compare_col", specification->compare_col); + e_xml_set_string_prop_by_name (node, (const guchar *)"_title", specification->title); + e_xml_set_string_prop_by_name (node, (const guchar *)"pixbuf", specification->pixbuf); + + e_xml_set_double_prop_by_name (node, (const guchar *)"expansion", specification->expansion); + e_xml_set_integer_prop_by_name (node, (const guchar *)"minimum_width", specification->minimum_width); + e_xml_set_bool_prop_by_name (node, (const guchar *)"resizable", specification->resizable); + e_xml_set_bool_prop_by_name (node, (const guchar *)"disabled", specification->disabled); + + e_xml_set_string_prop_by_name (node, (const guchar *)"cell", specification->cell); + e_xml_set_string_prop_by_name (node, (const guchar *)"compare", specification->compare); + e_xml_set_string_prop_by_name (node, (const guchar *)"search", specification->search); + if (specification->priority != 0) + e_xml_set_integer_prop_by_name (node, (const guchar *)"priority", specification->priority); + + return node; +} + diff --git a/e-util/e-table-column-specification.h b/e-util/e-table-column-specification.h new file mode 100644 index 0000000000..ae1a00cc65 --- /dev/null +++ b/e-util/e-table-column-specification.h @@ -0,0 +1,93 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_COLUMN_SPECIFICATION_H_ +#define _E_TABLE_COLUMN_SPECIFICATION_H_ + +#include <glib-object.h> +#include <libxml/tree.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_COLUMN_SPECIFICATION \ + (e_table_column_specification_get_type ()) +#define E_TABLE_COLUMN_SPECIFICATION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_COLUMN_SPECIFICATION, ETableColumnSpecification)) +#define E_TABLE_COLUMN_SPECIFICATION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_COLUMN_SPECIFICATION, ETableColumnSpecificationClass)) +#define E_IS_TABLE_COLUMN_SPECIFICATION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_COLUMN_SPECIFICATION)) +#define E_IS_TABLE_COLUMN_SPECIFICATION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_COLUMN_SPECIFICATION)) +#define E_TABLE_COLUMN_SPECIFICATION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_COLUMN_SPECIFICATION, ETableColumnSpecificationClass)) + +G_BEGIN_DECLS + +typedef struct _ETableColumnSpecification ETableColumnSpecification; +typedef struct _ETableColumnSpecificationClass ETableColumnSpecificationClass; + +struct _ETableColumnSpecification { + GObject parent; + + gint model_col; + gint compare_col; + gchar *title; + gchar *pixbuf; + + gdouble expansion; + gint minimum_width; + guint resizable : 1; + guint disabled : 1; + + gchar *cell; + gchar *compare; + gchar *search; + gchar *sortable; + gint priority; +}; + +struct _ETableColumnSpecificationClass { + GObjectClass parent_class; +}; + +GType e_table_column_specification_get_type (void) G_GNUC_CONST; +ETableColumnSpecification * + e_table_column_specification_new (void); +void e_table_column_specification_load_from_node + (ETableColumnSpecification *state, + const xmlNode *node); +xmlNode * e_table_column_specification_save_to_node + (ETableColumnSpecification *state, + xmlNode *parent); + +G_END_DECLS + +#endif /* _E_TABLE_COLUMN_SPECIFICATION_H_ */ diff --git a/e-util/e-table-config.c b/e-util/e-table-config.c new file mode 100644 index 0000000000..98f89ffd10 --- /dev/null +++ b/e-util/e-table-config.c @@ -0,0 +1,1481 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * FIXME: + * Sort Dialog: when text is selected, the toggle button switches state. + * Make Clear all work. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-config.h" + +#include <stdlib.h> +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "e-table-memory-store.h" +#include "e-table-without.h" +#include "e-unicode.h" +#include "e-util-private.h" + +G_DEFINE_TYPE (ETableConfig, e_table_config, G_TYPE_OBJECT) + +enum { + CHANGED, + LAST_SIGNAL +}; + +enum { + PROP_0, + PROP_STATE +}; + +enum { + COLUMN_ITEM, + COLUMN_VALUE +}; + +static guint e_table_config_signals[LAST_SIGNAL] = { 0, }; + +static void +config_finalize (GObject *object) +{ + ETableConfig *config = E_TABLE_CONFIG (object); + + if (config->state) + g_object_unref (config->state); + config->state = NULL; + + if (config->source_state) + g_object_unref (config->source_state); + config->source_state = NULL; + + if (config->source_spec) + g_object_unref (config->source_spec); + config->source_spec = NULL; + + g_free (config->header); + config->header = NULL; + + g_slist_free (config->column_names); + config->column_names = NULL; + + g_free (config->domain); + config->domain = NULL; + + G_OBJECT_CLASS (e_table_config_parent_class)->finalize (object); +} + +static void +e_table_config_changed (ETableConfig *config, + ETableState *state) +{ + g_return_if_fail (E_IS_TABLE_CONFIG (config)); + + g_signal_emit (config, e_table_config_signals[CHANGED], 0, state); +} + +static void +config_dialog_changed (ETableConfig *config) +{ + /* enable the apply/ok buttons */ + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (config->dialog_toplevel), + GTK_RESPONSE_APPLY, TRUE); + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (config->dialog_toplevel), + GTK_RESPONSE_OK, TRUE); +} + +static void +config_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableConfig *config = E_TABLE_CONFIG (object); + + switch (property_id) { + case PROP_STATE: + g_value_set_object (value, config->state); + break; + default: + break; + } +} + +static void +e_table_config_class_init (ETableConfigClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + class->changed = NULL; + + object_class->finalize = config_finalize; + object_class->get_property = config_get_property; + + e_table_config_signals[CHANGED] = g_signal_new ( + "changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableConfigClass, changed), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + g_object_class_install_property ( + object_class, + PROP_STATE, + g_param_spec_object ( + "state", + "State", + NULL, + E_TYPE_TABLE_STATE, + G_PARAM_READABLE)); +} + +static void +configure_combo_box_add (GtkComboBox *combo_box, + const gchar *item, + const gchar *value) +{ + GtkTreeRowReference *reference; + GtkTreeModel *model; + GtkTreePath *path; + GHashTable *index; + GtkTreeIter iter; + + model = gtk_combo_box_get_model (combo_box); + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + + gtk_list_store_set ( + GTK_LIST_STORE (model), &iter, + COLUMN_ITEM, item, COLUMN_VALUE, value, -1); + + index = g_object_get_data (G_OBJECT (combo_box), "index"); + g_return_if_fail (index != NULL); + + /* Add an entry to the tree model index. */ + path = gtk_tree_model_get_path (model, &iter); + reference = gtk_tree_row_reference_new (model, path); + g_return_if_fail (reference != NULL); + g_hash_table_insert (index, g_strdup (value), reference); + gtk_tree_path_free (path); +} + +static gchar * +configure_combo_box_get_active (GtkComboBox *combo_box) +{ + GtkTreeIter iter; + gchar *value = NULL; + + if (gtk_combo_box_get_active_iter (combo_box, &iter)) + gtk_tree_model_get ( + gtk_combo_box_get_model (combo_box), &iter, + COLUMN_VALUE, &value, -1); + + if (value != NULL && *value == '\0') { + g_free (value); + value = NULL; + } + + return value; +} + +static void +configure_combo_box_set_active (GtkComboBox *combo_box, + const gchar *value) +{ + GtkTreeRowReference *reference; + GHashTable *index; + + index = g_object_get_data (G_OBJECT (combo_box), "index"); + g_return_if_fail (index != NULL); + + reference = g_hash_table_lookup (index, value); + if (reference != NULL) { + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + + model = gtk_tree_row_reference_get_model (reference); + path = gtk_tree_row_reference_get_path (reference); + + if (path == NULL) + return; + + if (gtk_tree_model_get_iter (model, &iter, path)) + gtk_combo_box_set_active_iter (combo_box, &iter); + + gtk_tree_path_free (path); + } +} + +static ETableColumnSpecification * +find_column_in_spec (ETableSpecification *spec, + gint model_col) +{ + ETableColumnSpecification **column; + + for (column = spec->columns; *column; column++) { + if ((*column)->disabled) + continue; + if ((*column)->model_col != model_col) + continue; + + return *column; + } + + return NULL; +} + +static gint +find_model_column_by_name (ETableSpecification *spec, + const gchar *s) +{ + ETableColumnSpecification **column; + + for (column = spec->columns; *column; column++) { + + if ((*column)->disabled) + continue; + if (g_ascii_strcasecmp ((*column)->title, s) == 0) + return (*column)->model_col; + } + return -1; +} + +static void +update_sort_and_group_config_dialog (ETableConfig *config, + gboolean is_sort) +{ + ETableConfigSortWidgets *widgets; + gint count, i; + + if (is_sort) { + count = e_table_sort_info_sorting_get_count ( + config->temp_state->sort_info); + widgets = &config->sort[0]; + } else { + count = e_table_sort_info_grouping_get_count ( + config->temp_state->sort_info); + widgets = &config->group[0]; + } + + for (i = 0; i < 4; i++) { + gboolean sensitive = (i <= count); + const gchar *text = ""; + + gtk_widget_set_sensitive (widgets[i].frames, sensitive); + + /* + * Sorting is set, auto select the text + */ + g_signal_handler_block ( + widgets[i].radio_ascending, + widgets[i].toggled_id); + g_signal_handler_block ( + widgets[i].combo, + widgets[i].changed_id); + + if (i < count) { + GtkToggleButton *a, *d; + ETableSortColumn col = + is_sort + ? e_table_sort_info_sorting_get_nth ( + config->temp_state->sort_info, + i) + : e_table_sort_info_grouping_get_nth ( + config->temp_state->sort_info, + i); + + ETableColumnSpecification *column = + find_column_in_spec (config->source_spec, col.column); + + if (!column) { + /* + * This is a bug in the programmer + * stuff, but by the time we arrive + * here, the user has been given a + * warning + */ + continue; + } + + text = column->title; + + /* + * Update radio buttons + */ + a = GTK_TOGGLE_BUTTON ( + widgets[i].radio_ascending); + d = GTK_TOGGLE_BUTTON ( + widgets[i].radio_descending); + + gtk_toggle_button_set_active (col.ascending ? a : d, 1); + } else { + GtkToggleButton *t; + + t = GTK_TOGGLE_BUTTON ( + widgets[i].radio_ascending); + + if (is_sort) + g_return_if_fail ( + widgets[i].radio_ascending != + config->group[i].radio_ascending); + else + g_return_if_fail ( + widgets[i].radio_ascending != + config->sort[i].radio_ascending); + gtk_toggle_button_set_active (t, 1); + } + + /* Set the text */ + configure_combo_box_set_active ( + GTK_COMBO_BOX (widgets[i].combo), text); + + g_signal_handler_unblock ( + widgets[i].radio_ascending, + widgets[i].toggled_id); + g_signal_handler_unblock ( + widgets[i].combo, + widgets[i].changed_id); + } +} + +static void +config_sort_info_update (ETableConfig *config) +{ + ETableSortInfo *info = config->state->sort_info; + GString *res; + gint count, i; + + count = e_table_sort_info_sorting_get_count (info); + res = g_string_new (""); + + for (i = 0; i < count; i++) { + ETableSortColumn col = e_table_sort_info_sorting_get_nth (info, i); + ETableColumnSpecification *column; + + column = find_column_in_spec (config->source_spec, col.column); + if (!column) { + g_warning ("Could not find column model in specification"); + continue; + } + + g_string_append (res, dgettext (config->domain, (column)->title)); + g_string_append_c (res, ' '); + g_string_append ( + res, + col.ascending ? + _("(Ascending)") : _("(Descending)")); + + if ((i + 1) != count) + g_string_append (res, ", "); + } + + if (res->str[0] == 0) + g_string_append (res, _("Not sorted")); + + gtk_label_set_text (GTK_LABEL (config->sort_label), res->str); + + g_string_free (res, TRUE); +} + +static void +config_group_info_update (ETableConfig *config) +{ + ETableSortInfo *info = config->state->sort_info; + GString *res; + gint count, i; + + if (!e_table_sort_info_get_can_group (info)) + return; + + count = e_table_sort_info_grouping_get_count (info); + res = g_string_new (""); + + for (i = 0; i < count; i++) { + ETableSortColumn col = e_table_sort_info_grouping_get_nth (info, i); + ETableColumnSpecification *column; + + column = find_column_in_spec (config->source_spec, col.column); + if (!column) { + g_warning ("Could not find model column in specification"); + continue; + } + + g_string_append (res, dgettext (config->domain, (column)->title)); + g_string_append_c (res, ' '); + g_string_append ( + res, + col.ascending ? + _("(Ascending)") : _("(Descending)")); + + if ((i + 1) != count) + g_string_append (res, ", "); + } + if (res->str[0] == 0) + g_string_append (res, _("No grouping")); + + gtk_label_set_text (GTK_LABEL (config->group_label), res->str); + g_string_free (res, TRUE); +} + +static void +setup_fields (ETableConfig *config) +{ + gint i; + + e_table_model_freeze ((ETableModel *) config->available_model); + e_table_model_freeze ((ETableModel *) config->shown_model); + e_table_without_show_all (config->available_model); + e_table_subset_variable_clear (config->shown_model); + + if (config->temp_state) { + for (i = 0; i < config->temp_state->col_count; i++) { + gint j, idx; + for (j = 0, idx = 0; j < config->temp_state->columns[i]; j++) + if (!config->source_spec->columns[j]->disabled) + idx++; + + e_table_subset_variable_add (config->shown_model, idx); + e_table_without_hide (config->available_model, GINT_TO_POINTER (idx)); + } + } + e_table_model_thaw ((ETableModel *) config->available_model); + e_table_model_thaw ((ETableModel *) config->shown_model); +} + +static void +config_fields_info_update (ETableConfig *config) +{ + ETableColumnSpecification **column; + GString *res = g_string_new (""); + gint i, j; + + for (i = 0; i < config->state->col_count; i++) { + for (j = 0, column = config->source_spec->columns; *column; column++, j++) { + + if ((*column)->disabled) + continue; + + if (config->state->columns[i] != j) + continue; + + g_string_append (res, dgettext (config->domain, (*column)->title)); + if (i + 1 < config->state->col_count) + g_string_append (res, ", "); + + break; + } + } + + gtk_label_set_text (GTK_LABEL (config->fields_label), res->str); + g_string_free (res, TRUE); +} + +static void +do_sort_and_group_config_dialog (ETableConfig *config, + gboolean is_sort) +{ + GtkDialog *dialog; + gint response, running = 1; + + config->temp_state = e_table_state_duplicate (config->state); + + update_sort_and_group_config_dialog (config, is_sort); + + gtk_widget_grab_focus (GTK_WIDGET ( + is_sort + ? config->sort[0].combo + : config->group[0].combo)); + + if (is_sort) + dialog = GTK_DIALOG (config->dialog_sort); + else + dialog = GTK_DIALOG (config->dialog_group_by); + + gtk_window_set_transient_for ( + GTK_WINDOW (dialog), GTK_WINDOW (config->dialog_toplevel)); + + do { + response = gtk_dialog_run (dialog); + switch (response) { + case 0: /* clear fields */ + if (is_sort) { + e_table_sort_info_sorting_truncate ( + config->temp_state->sort_info, 0); + } else { + e_table_sort_info_grouping_truncate ( + config->temp_state->sort_info, 0); + } + update_sort_and_group_config_dialog (config, is_sort); + break; + + case GTK_RESPONSE_OK: + g_object_unref (config->state); + config->state = config->temp_state; + config->temp_state = NULL; + running = 0; + config_dialog_changed (config); + break; + + case GTK_RESPONSE_DELETE_EVENT: + case GTK_RESPONSE_CANCEL: + g_object_unref (config->temp_state); + config->temp_state = NULL; + running = 0; + break; + } + + } while (running); + gtk_widget_hide (GTK_WIDGET (dialog)); + + if (is_sort) + config_sort_info_update (config); + else + config_group_info_update (config); +} + +static void +do_fields_config_dialog (ETableConfig *config) +{ + GtkDialog *dialog; + GtkWidget *widget; + gint response, running = 1; + + dialog = GTK_DIALOG (config->dialog_show_fields); + + gtk_widget_ensure_style (config->dialog_show_fields); + + widget = gtk_dialog_get_content_area (dialog); + gtk_container_set_border_width (GTK_CONTAINER (widget), 0); + + widget = gtk_dialog_get_action_area (dialog); + gtk_container_set_border_width (GTK_CONTAINER (widget), 12); + + config->temp_state = e_table_state_duplicate (config->state); + + setup_fields (config); + + gtk_window_set_transient_for ( + GTK_WINDOW (config->dialog_show_fields), + GTK_WINDOW (config->dialog_toplevel)); + + do { + response = gtk_dialog_run (GTK_DIALOG (config->dialog_show_fields)); + switch (response) { + case GTK_RESPONSE_OK: + g_object_unref (config->state); + config->state = config->temp_state; + config->temp_state = NULL; + running = 0; + config_dialog_changed (config); + break; + + case GTK_RESPONSE_DELETE_EVENT: + case GTK_RESPONSE_CANCEL: + g_object_unref (config->temp_state); + config->temp_state = NULL; + running = 0; + break; + } + + } while (running); + gtk_widget_hide (GTK_WIDGET (config->dialog_show_fields)); + + config_fields_info_update (config); +} + +static ETableMemoryStoreColumnInfo store_columns[] = { + E_TABLE_MEMORY_STORE_STRING, + E_TABLE_MEMORY_STORE_INTEGER, + E_TABLE_MEMORY_STORE_TERMINATOR +}; + +static ETableModel * +create_store (ETableConfig *config) +{ + gint i; + ETableModel *store; + + store = e_table_memory_store_new (store_columns); + for (i = 0; config->source_spec->columns[i]; i++) { + + gchar *text; + + if (config->source_spec->columns[i]->disabled) + continue; + + text = g_strdup (dgettext ( + config->domain, + config->source_spec->columns[i]->title)); + e_table_memory_store_insert_adopt ( + E_TABLE_MEMORY_STORE (store), -1, NULL, text, i); + } + + return store; +} + +static const gchar *spec = +"<ETableSpecification gettext-domain=\"" GETTEXT_PACKAGE "\"" +" no-headers=\"true\" cursor-mode=\"line\" draw-grid=\"false\" " +" draw-focus=\"true\" selection-mode=\"browse\">" +"<ETableColumn model_col= \"0\" _title=\"Name\" minimum_width=\"30\"" +" resizable=\"true\" cell=\"string\" compare=\"string\"/>" +"<ETableState> <column source=\"0\"/>" +"<grouping/>" +"</ETableState>" +"</ETableSpecification>"; + +static GtkWidget * +e_table_proxy_etable_shown_new (ETableModel *store) +{ + ETableModel *model = NULL; + GtkWidget *widget; + + model = e_table_subset_variable_new (store); + + widget = e_table_new (model, NULL, spec, NULL); + + atk_object_set_name ( + gtk_widget_get_accessible (widget), + _("Show Fields")); + + return widget; +} + +static GtkWidget * +e_table_proxy_etable_available_new (ETableModel *store) +{ + ETableModel *model; + GtkWidget *widget; + + model = e_table_without_new ( + store, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + + e_table_without_show_all (E_TABLE_WITHOUT (model)); + + widget = e_table_new (model, NULL, spec, NULL); + + atk_object_set_name ( + gtk_widget_get_accessible (widget), + _("Available Fields")); + + return widget; +} + +static void +config_button_fields (GtkWidget *widget, + ETableConfig *config) +{ + do_fields_config_dialog (config); +} + +static void +config_button_sort (GtkWidget *widget, + ETableConfig *config) +{ + do_sort_and_group_config_dialog (config, TRUE); +} + +static void +config_button_group (GtkWidget *widget, + ETableConfig *config) +{ + do_sort_and_group_config_dialog (config, FALSE); +} + +static void +dialog_destroyed (gpointer data, + GObject *where_object_was) +{ + ETableConfig *config = data; + g_object_unref (config); +} + +static void +dialog_response (GtkWidget *dialog, + gint response_id, + ETableConfig *config) +{ + if (response_id == GTK_RESPONSE_APPLY + || response_id == GTK_RESPONSE_OK) { + e_table_config_changed (config, config->state); + } + + if (response_id == GTK_RESPONSE_CANCEL + || response_id == GTK_RESPONSE_OK) { + gtk_widget_destroy (dialog); + } +} + +/* + * Invoked by the GtkBuilder auto-connect code + */ +static GtkWidget * +e_table_proxy_gtk_combo_text_new (void) +{ + GtkCellRenderer *renderer; + GtkListStore *store; + GtkWidget *combo_box; + GHashTable *index; + + store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING); + combo_box = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (combo_box), renderer, FALSE); + gtk_cell_layout_add_attribute ( + GTK_CELL_LAYOUT (combo_box), renderer, "text", COLUMN_ITEM); + + /* Embed a reverse-lookup index into the widget. */ + index = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) gtk_tree_row_reference_free); + g_object_set_data_full ( + G_OBJECT (combo_box), "index", index, + (GDestroyNotify) g_hash_table_destroy); + + return combo_box; +} + +static void +connect_button (ETableConfig *config, + GtkBuilder *builder, + const gchar *widget_name, + GCallback cback) +{ + GtkWidget *button = e_builder_get_widget (builder, widget_name); + + if (button) + g_signal_connect (button, "clicked", cback, config); +} + +static gint +get_source_model_col_index (ETableConfig *config, + gint idx) +{ + gint visible_index; + ETableModel *src_model; + + src_model = E_TABLE_SUBSET (config->available_model)->source; + + visible_index = e_table_subset_view_to_model_row ( + E_TABLE_SUBSET (config->available_model), idx); + + return GPOINTER_TO_INT (e_table_model_value_at (src_model, 1, visible_index)); +} + +static void +sort_combo_changed (GtkComboBox *combo_box, + ETableConfigSortWidgets *sort) +{ + ETableConfig *config = sort->e_table_config; + ETableSortInfo *sort_info = config->temp_state->sort_info; + ETableConfigSortWidgets *base = &config->sort[0]; + GtkToggleButton *toggle_button; + gint idx = sort - base; + gchar *s; + + s = configure_combo_box_get_active (combo_box); + + if (s != NULL) { + ETableSortColumn c; + gint col; + + col = find_model_column_by_name (config->source_spec, s); + if (col == -1) { + g_warning ("sort: This should not happen (%s)", s); + g_free (s); + return; + } + + toggle_button = GTK_TOGGLE_BUTTON ( + config->sort[idx].radio_ascending); + c.ascending = gtk_toggle_button_get_active (toggle_button); + c.column = col; + e_table_sort_info_sorting_set_nth (sort_info, idx, c); + + update_sort_and_group_config_dialog (config, TRUE); + } else { + e_table_sort_info_sorting_truncate (sort_info, idx); + update_sort_and_group_config_dialog (config, TRUE); + } + + g_free (s); +} + +static void +sort_ascending_toggled (GtkToggleButton *t, + ETableConfigSortWidgets *sort) +{ + ETableConfig *config = sort->e_table_config; + ETableSortInfo *si = config->temp_state->sort_info; + ETableConfigSortWidgets *base = &config->sort[0]; + gint idx = sort - base; + ETableSortColumn c; + + c = e_table_sort_info_sorting_get_nth (si, idx); + c.ascending = gtk_toggle_button_get_active (t); + e_table_sort_info_sorting_set_nth (si, idx, c); +} + +static void +configure_sort_dialog (ETableConfig *config, + GtkBuilder *builder) +{ + GSList *l; + gint i; + + const gchar *algs[] = { + "alignment4", + "alignment3", + "alignment2", + "alignment1", + NULL + }; + + for (i = 0; i < 4; i++) { + gchar buffer[80]; + + snprintf (buffer, sizeof (buffer), "sort-combo-%d", i + 1); + config->sort[i].combo = e_table_proxy_gtk_combo_text_new (); + gtk_widget_show (GTK_WIDGET (config->sort[i].combo)); + gtk_container_add ( + GTK_CONTAINER (e_builder_get_widget ( + builder, algs[i])), config->sort[i].combo); + configure_combo_box_add ( + GTK_COMBO_BOX (config->sort[i].combo), "", ""); + + snprintf (buffer, sizeof (buffer), "frame-sort-%d", i + 1); + config->sort[i].frames = + e_builder_get_widget (builder, buffer); + + snprintf ( + buffer, sizeof (buffer), + "radiobutton-ascending-sort-%d", i + 1); + config->sort[i].radio_ascending = e_builder_get_widget ( + builder, buffer); + + snprintf ( + buffer, sizeof (buffer), + "radiobutton-descending-sort-%d", i + 1); + config->sort[i].radio_descending = e_builder_get_widget ( + builder, buffer); + + config->sort[i].e_table_config = config; + } + + for (l = config->column_names; l; l = l->next) { + gchar *label = l->data; + + for (i = 0; i < 4; i++) { + configure_combo_box_add ( + GTK_COMBO_BOX (config->sort[i].combo), + dgettext (config->domain, label), label); + } + } + + /* + * After we have runtime modified things, signal connect + */ + for (i = 0; i < 4; i++) { + config->sort[i].changed_id = g_signal_connect ( + config->sort[i].combo, + "changed", G_CALLBACK (sort_combo_changed), + &config->sort[i]); + + config->sort[i].toggled_id = g_signal_connect ( + config->sort[i].radio_ascending, + "toggled", G_CALLBACK (sort_ascending_toggled), + &config->sort[i]); + } +} + +static void +group_combo_changed (GtkComboBox *combo_box, + ETableConfigSortWidgets *group) +{ + ETableConfig *config = group->e_table_config; + ETableSortInfo *sort_info = config->temp_state->sort_info; + ETableConfigSortWidgets *base = &config->group[0]; + gint idx = group - base; + gchar *s; + + s = configure_combo_box_get_active (combo_box); + + if (s != NULL) { + GtkToggleButton *toggle_button; + ETableSortColumn c; + gint col; + + col = find_model_column_by_name (config->source_spec, s); + if (col == -1) { + g_warning ("grouping: this should not happen, %s", s); + g_free (s); + return; + } + + toggle_button = GTK_TOGGLE_BUTTON ( + config->group[idx].radio_ascending); + c.ascending = gtk_toggle_button_get_active (toggle_button); + c.column = col; + e_table_sort_info_grouping_set_nth (sort_info, idx, c); + + update_sort_and_group_config_dialog (config, FALSE); + } else { + e_table_sort_info_grouping_truncate (sort_info, idx); + update_sort_and_group_config_dialog (config, FALSE); + } + + g_free (s); +} + +static void +group_ascending_toggled (GtkToggleButton *t, + ETableConfigSortWidgets *group) +{ + ETableConfig *config = group->e_table_config; + ETableSortInfo *si = config->temp_state->sort_info; + ETableConfigSortWidgets *base = &config->group[0]; + gint idx = group - base; + ETableSortColumn c; + + c = e_table_sort_info_grouping_get_nth (si, idx); + c.ascending = gtk_toggle_button_get_active (t); + e_table_sort_info_grouping_set_nth (si, idx, c); +} + +static void +configure_group_dialog (ETableConfig *config, + GtkBuilder *builder) +{ + GSList *l; + gint i; + const gchar *vboxes[] = {"vbox7", "vbox9", "vbox11", "vbox13", NULL}; + + for (i = 0; i < 4; i++) { + gchar buffer[80]; + + snprintf (buffer, sizeof (buffer), "group-combo-%d", i + 1); + config->group[i].combo = e_table_proxy_gtk_combo_text_new (); + gtk_widget_show (GTK_WIDGET (config->group[i].combo)); + gtk_box_pack_start ( + GTK_BOX (e_builder_get_widget (builder, vboxes[i])), + config->group[i].combo, FALSE, FALSE, 0); + + configure_combo_box_add ( + GTK_COMBO_BOX (config->group[i].combo), "", ""); + + snprintf (buffer, sizeof (buffer), "frame-group-%d", i + 1); + config->group[i].frames = + e_builder_get_widget (builder, buffer); + + snprintf ( + buffer, sizeof (buffer), + "radiobutton-ascending-group-%d", i + 1); + config->group[i].radio_ascending = e_builder_get_widget ( + builder, buffer); + + snprintf ( + buffer, sizeof (buffer), + "radiobutton-descending-group-%d", i + 1); + config->group[i].radio_descending = e_builder_get_widget ( + builder, buffer); + + snprintf ( + buffer, sizeof (buffer), + "checkbutton-group-%d", i + 1); + config->group[i].view_check = e_builder_get_widget ( + builder, buffer); + + config->group[i].e_table_config = config; + } + + for (l = config->column_names; l; l = l->next) { + gchar *label = l->data; + + for (i = 0; i < 4; i++) { + configure_combo_box_add ( + GTK_COMBO_BOX (config->group[i].combo), + dgettext (config->domain, label), label); + } + } + + /* + * After we have runtime modified things, signal connect + */ + for (i = 0; i < 4; i++) { + config->group[i].changed_id = g_signal_connect ( + config->group[i].combo, + "changed", G_CALLBACK (group_combo_changed), + &config->group[i]); + + config->group[i].toggled_id = g_signal_connect ( + config->group[i].radio_ascending, + "toggled", G_CALLBACK (group_ascending_toggled), + &config->group[i]); + } +} + +static void +add_column (gint model_row, + gpointer closure) +{ + GList **columns = closure; + *columns = g_list_prepend (*columns, GINT_TO_POINTER (model_row)); +} + +static void +config_button_add (GtkWidget *widget, + ETableConfig *config) +{ + GList *columns = NULL; + GList *column; + gint count; + gint i; + + e_table_selected_row_foreach (config->available, add_column, &columns); + columns = g_list_reverse (columns); + + count = g_list_length (columns); + + config->temp_state->columns = g_renew ( + int, config->temp_state->columns, + config->temp_state->col_count + count); + config->temp_state->expansions = g_renew ( + gdouble, config->temp_state->expansions, + config->temp_state->col_count + count); + i = config->temp_state->col_count; + for (column = columns; column; column = column->next) { + config->temp_state->columns[i] = + get_source_model_col_index ( + config, GPOINTER_TO_INT (column->data)); + config->temp_state->expansions[i] = + config->source_spec->columns + [config->temp_state->columns[i]]->expansion; + i++; + } + config->temp_state->col_count += count; + + g_list_free (columns); + + setup_fields (config); +} + +static void +config_button_remove (GtkWidget *widget, + ETableConfig *config) +{ + GList *columns = NULL; + GList *column; + + e_table_selected_row_foreach (config->shown, add_column, &columns); + + for (column = columns; column; column = column->next) { + gint row = GPOINTER_TO_INT (column->data); + + memmove ( + config->temp_state->columns + row, + config->temp_state->columns + row + 1, + sizeof (gint) * (config->temp_state->col_count - row - 1)); + memmove ( + config->temp_state->expansions + row, + config->temp_state->expansions + row + 1, + sizeof (gdouble) * (config->temp_state->col_count - row - 1)); + config->temp_state->col_count--; + } + config->temp_state->columns = g_renew ( + int, config->temp_state->columns, + config->temp_state->col_count); + config->temp_state->expansions = g_renew ( + gdouble, config->temp_state->expansions, + config->temp_state->col_count); + + g_list_free (columns); + + setup_fields (config); +} + +static void +config_button_up (GtkWidget *widget, + ETableConfig *config) +{ + GList *columns = NULL; + GList *column; + gint *new_shown; + gdouble *new_expansions; + gint next_col; + gdouble next_expansion; + gint i; + + e_table_selected_row_foreach (config->shown, add_column, &columns); + + /* if no columns left, just return */ + if (columns == NULL) + return; + + columns = g_list_reverse (columns); + + new_shown = g_new (int, config->temp_state->col_count); + new_expansions = g_new (double, config->temp_state->col_count); + + column = columns; + + next_col = config->temp_state->columns[0]; + next_expansion = config->temp_state->expansions[0]; + + for (i = 1; i < config->temp_state->col_count; i++) { + if (column && (GPOINTER_TO_INT (column->data) == i)) { + new_expansions[i - 1] = config->temp_state->expansions[i]; + new_shown[i - 1] = config->temp_state->columns[i]; + column = column->next; + } else { + new_shown[i - 1] = next_col; + next_col = config->temp_state->columns[i]; + + new_expansions[i - 1] = next_expansion; + next_expansion = config->temp_state->expansions[i]; + } + } + + new_shown[i - 1] = next_col; + new_expansions[i - 1] = next_expansion; + + g_free (config->temp_state->columns); + g_free (config->temp_state->expansions); + + config->temp_state->columns = new_shown; + config->temp_state->expansions = new_expansions; + + g_list_free (columns); + + setup_fields (config); +} + +static void +config_button_down (GtkWidget *widget, + ETableConfig *config) +{ + GList *columns = NULL; + GList *column; + gint *new_shown; + gdouble *new_expansions; + gint next_col; + gdouble next_expansion; + gint i; + + e_table_selected_row_foreach (config->shown, add_column, &columns); + + /* if no columns left, just return */ + if (columns == NULL) + return; + + new_shown = g_new (gint, config->temp_state->col_count); + new_expansions = g_new (gdouble, config->temp_state->col_count); + + column = columns; + + next_col = + config->temp_state->columns[config->temp_state->col_count - 1]; + next_expansion = + config->temp_state->expansions[config->temp_state->col_count - 1]; + + for (i = config->temp_state->col_count - 1; i > 0; i--) { + if (column && (GPOINTER_TO_INT (column->data) == i - 1)) { + new_expansions[i] = config->temp_state->expansions[i - 1]; + new_shown[i] = config->temp_state->columns[i - 1]; + column = column->next; + } else { + new_shown[i] = next_col; + next_col = config->temp_state->columns[i - 1]; + + new_expansions[i] = next_expansion; + next_expansion = config->temp_state->expansions[i - 1]; + } + } + + new_shown[0] = next_col; + new_expansions[0] = next_expansion; + + g_free (config->temp_state->columns); + g_free (config->temp_state->expansions); + + config->temp_state->columns = new_shown; + config->temp_state->expansions = new_expansions; + + g_list_free (columns); + + setup_fields (config); +} + +static void +configure_fields_dialog (ETableConfig *config, + GtkBuilder *builder) +{ + GtkWidget *scrolled; + GtkWidget *etable; + ETableModel *store = create_store (config); + + /* "custom-available" widget */ + etable = e_table_proxy_etable_available_new (store); + gtk_widget_show (etable); + scrolled = e_builder_get_widget (builder, "available-scrolled"); + gtk_container_add (GTK_CONTAINER (scrolled), etable); + config->available = E_TABLE (etable); + g_object_get ( + config->available, + "model", &config->available_model, + NULL); + gtk_widget_show_all (etable); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (e_builder_get_widget ( + builder, "label-available")), etable); + + /* "custom-shown" widget */ + etable = e_table_proxy_etable_shown_new (store); + gtk_widget_show (etable); + scrolled = e_builder_get_widget (builder, "shown-scrolled"); + gtk_container_add (GTK_CONTAINER (scrolled), etable); + config->shown = E_TABLE (etable); + g_object_get ( + config->shown, + "model", &config->shown_model, + NULL); + gtk_widget_show_all (etable); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (e_builder_get_widget ( + builder, "label-displayed")), etable); + + connect_button ( + config, builder, "button-add", + G_CALLBACK (config_button_add)); + connect_button ( + config, builder, "button-remove", + G_CALLBACK (config_button_remove)); + connect_button ( + config, builder, "button-up", + G_CALLBACK (config_button_up)); + connect_button ( + config, builder, "button-down", + G_CALLBACK (config_button_down)); + + setup_fields (config); + + g_object_unref (store); +} + +static void +setup_gui (ETableConfig *config) +{ + GtkBuilder *builder; + gboolean can_group; + + can_group = e_table_sort_info_get_can_group (config->state->sort_info); + builder = gtk_builder_new (); + e_load_ui_builder_definition (builder, "e-table-config.ui"); + + config->dialog_toplevel = e_builder_get_widget ( + builder, "e-table-config"); + + if (config->header) + gtk_window_set_title ( + GTK_WINDOW (config->dialog_toplevel), + config->header); + + config->dialog_show_fields = e_builder_get_widget ( + builder, "dialog-show-fields"); + config->dialog_group_by = e_builder_get_widget ( + builder, "dialog-group-by"); + config->dialog_sort = e_builder_get_widget ( + builder, "dialog-sort"); + + config->sort_label = e_builder_get_widget ( + builder, "label-sort"); + config->group_label = e_builder_get_widget ( + builder, "label-group"); + config->fields_label = e_builder_get_widget ( + builder, "label-fields"); + + connect_button ( + config, builder, "button-sort", + G_CALLBACK (config_button_sort)); + connect_button ( + config, builder, "button-group", + G_CALLBACK (config_button_group)); + connect_button ( + config, builder, "button-fields", + G_CALLBACK (config_button_fields)); + + if (!can_group) { + GtkWidget *w; + + w = e_builder_get_widget (builder, "button-group"); + if (w) + gtk_widget_hide (w); + + w = e_builder_get_widget (builder, "label3"); + if (w) + gtk_widget_hide (w); + + w = config->group_label; + if (w) + gtk_widget_hide (w); + } + + configure_sort_dialog (config, builder); + configure_group_dialog (config, builder); + configure_fields_dialog (config, builder); + + g_object_weak_ref ( + G_OBJECT (config->dialog_toplevel), + dialog_destroyed, config); + + g_signal_connect ( + config->dialog_toplevel, "response", + G_CALLBACK (dialog_response), config); + + g_object_unref (builder); +} + +static void +e_table_config_init (ETableConfig *config) +{ + config->domain = NULL; +} + +ETableConfig * +e_table_config_construct (ETableConfig *config, + const gchar *header, + ETableSpecification *spec, + ETableState *state, + GtkWindow *parent_window) +{ + ETableColumnSpecification **column; + + g_return_val_if_fail (config != NULL, NULL); + g_return_val_if_fail (header != NULL, NULL); + g_return_val_if_fail (spec != NULL, NULL); + g_return_val_if_fail (state != NULL, NULL); + + config->source_spec = spec; + config->source_state = state; + config->header = g_strdup (header); + + g_object_ref (config->source_spec); + g_object_ref (config->source_state); + + config->state = e_table_state_duplicate (state); + + config->domain = g_strdup (spec->domain); + + for (column = config->source_spec->columns; *column; column++) { + gchar *label = (*column)->title; + + if ((*column)->disabled) + continue; + + config->column_names = g_slist_append ( + config->column_names, label); + } + + setup_gui (config); + + gtk_window_set_transient_for (GTK_WINDOW (config->dialog_toplevel), + parent_window); + + config_sort_info_update (config); + config_group_info_update (config); + config_fields_info_update (config); + + return E_TABLE_CONFIG (config); +} + +/** + * e_table_config_new: + * @header: The title of the dialog for the ETableConfig. + * @spec: The specification for the columns to allow. + * @state: The current state of the configuration. + * + * Creates a new ETable config object. + * + * Returns: The config object. + */ +ETableConfig * +e_table_config_new (const gchar *header, + ETableSpecification *spec, + ETableState *state, + GtkWindow *parent_window) +{ + ETableConfig *config; + GtkDialog *dialog; + GtkWidget *widget; + + config = g_object_new (E_TYPE_TABLE_CONFIG, NULL); + + e_table_config_construct ( + config, header, spec, state, parent_window); + + dialog = GTK_DIALOG (config->dialog_toplevel); + + gtk_widget_ensure_style (config->dialog_toplevel); + + widget = gtk_dialog_get_content_area (dialog); + gtk_container_set_border_width (GTK_CONTAINER (widget), 0); + + widget = gtk_dialog_get_action_area (dialog); + gtk_container_set_border_width (GTK_CONTAINER (widget), 12); + + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (config->dialog_toplevel), + GTK_RESPONSE_APPLY, FALSE); + gtk_widget_show (config->dialog_toplevel); + + return E_TABLE_CONFIG (config); +} + +/** + * e_table_config_raise: + * @config: The ETableConfig object. + * + * Raises the dialog associated with this ETableConfig object. + */ +void +e_table_config_raise (ETableConfig *config) +{ + GdkWindow *window; + + window = gtk_widget_get_window (GTK_WIDGET (config->dialog_toplevel)); + gdk_window_raise (window); +} + diff --git a/e-util/e-table-config.h b/e-util/e-table-config.h new file mode 100644 index 0000000000..7fc74d9f27 --- /dev/null +++ b/e-util/e-table-config.h @@ -0,0 +1,134 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_CONFIG_H_ +#define _E_TABLE_CONFIG_H_ + +#include <gtk/gtk.h> + +#include <e-util/e-table-sort-info.h> +#include <e-util/e-table-specification.h> +#include <e-util/e-table-subset-variable.h> +#include <e-util/e-table-without.h> +#include <e-util/e-table.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_CONFIG \ + (e_table_config_get_type ()) +#define E_TABLE_CONFIG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_CONFIG, ETableConfig)) +#define E_TABLE_CONFIG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_CONFIG, ETableConfigClass)) +#define E_IS_TABLE_CONFIG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_CONFIG)) +#define E_IS_TABLE_CONFIG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_CONFIG)) +#define E_TABLE_CONFIG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_CONFIG, ETableConfigClass)) + +G_BEGIN_DECLS + +typedef struct _ETableConfigSortWidgets ETableConfigSortWidgets; + +typedef struct _ETableConfig ETableConfig; +typedef struct _ETableConfigClass ETableConfigClass; + +struct _ETableConfigSortWidgets { + GtkWidget *combo; + GtkWidget *frames; + GtkWidget *radio_ascending; + GtkWidget *radio_descending; + GtkWidget *view_check; /* Only for group dialog */ + guint changed_id, toggled_id; + gpointer e_table_config; +}; + +struct _ETableConfig { + GObject parent; + + gchar *header; + + /* + * Our various dialog boxes + */ + GtkWidget *dialog_toplevel; + GtkWidget *dialog_show_fields; + GtkWidget *dialog_group_by; + GtkWidget *dialog_sort; + + /* + * The state we manipulate + */ + ETableSpecification *source_spec; + ETableState *source_state, *state, *temp_state; + + GtkWidget *sort_label; + GtkWidget *group_label; + GtkWidget *fields_label; + + ETableConfigSortWidgets sort[4]; + ETableConfigSortWidgets group[4]; + + ETable *available; + ETableWithout *available_model; + ETable *shown; + ETableSubsetVariable *shown_model; + gchar *domain; + + /* + * List of valid column names + */ + GSList *column_names; +}; + +struct _ETableConfigClass { + GObjectClass parent_class; + + /* Signals */ + void (*changed) (ETableConfig *config); +}; + +GType e_table_config_get_type (void) G_GNUC_CONST; +ETableConfig * e_table_config_new (const gchar *header, + ETableSpecification *spec, + ETableState *state, + GtkWindow *parent_window); +ETableConfig *e_table_config_construct (ETableConfig *etco, + const gchar *header, + ETableSpecification *spec, + ETableState *state, + GtkWindow *parent_window); +void e_table_config_raise (ETableConfig *config); + +G_END_DECLS + +#endif /* _E_TABLE_CONFIG_H */ diff --git a/e-util/e-table-config.ui b/e-util/e-table-config.ui new file mode 100644 index 0000000000..cfc6cb57fc --- /dev/null +++ b/e-util/e-table-config.ui @@ -0,0 +1,1594 @@ +<?xml version="1.0"?> +<interface> + <!-- interface-requires gtk+ 2.12 --> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkDialog" id="dialog-show-fields"> + <property name="title" translatable="yes">Show Fields</property> + <property name="modal">True</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox3"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">8</property> + <child> + <object class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkTable" id="table2"> + <property name="visible">True</property> + <property name="n_columns">5</property> + <property name="column_spacing">6</property> + <property name="row_spacing">6</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkLabel" id="label-available"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">A_vailable Fields:</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="right_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label-displayed"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Show these fields in order:</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="left_attach">3</property> + <property name="right_attach">5</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table3"> + <property name="visible">True</property> + <property name="n_columns">5</property> + <property name="column_spacing">6</property> + <property name="row_spacing">6</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkVBox" id="vbox4"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkScrolledWindow" id="available-scrolled"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="right_attach">2</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox5"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkScrolledWindow" id="shown-scrolled"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox4"> + <property name="visible">True</property> + <property name="spacing">6</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkButton" id="button-up"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <child> + <object class="GtkAlignment" id="alignment7"> + <property name="visible">True</property> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <object class="GtkHBox" id="hbox19"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <object class="GtkImage" id="image3"> + <property name="visible">True</property> + <property name="stock">gtk-go-up</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label29"> + <property name="visible">True</property> + <property name="label" translatable="yes">Move _Up</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button-down"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <child> + <object class="GtkAlignment" id="alignment8"> + <property name="visible">True</property> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <object class="GtkHBox" id="hbox20"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <object class="GtkImage" id="image4"> + <property name="visible">True</property> + <property name="stock">gtk-go-down</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label30"> + <property name="visible">True</property> + <property name="label" translatable="yes">Move _Down</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">3</property> + <property name="right_attach">5</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox6"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkButton" id="button-add"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <child> + <object class="GtkAlignment" id="alignment5"> + <property name="visible">True</property> + <property name="xalign">0.69999998807907104</property> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <object class="GtkHBox" id="hbox17"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <object class="GtkLabel" id="label31"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Add</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkImage" id="image5"> + <property name="visible">True</property> + <property name="stock">gtk-go-forward</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button-remove"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <child> + <object class="GtkAlignment" id="alignment6"> + <property name="visible">True</property> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <object class="GtkHBox" id="hbox18"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-go-back</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label27"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Remove</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area3"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button22"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button20"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">button22</action-widget> + <action-widget response="-5">button20</action-widget> + </action-widgets> + </object> + <object class="GtkDialog" id="dialog-group-by"> + <property name="title" translatable="yes">Group</property> + <property name="modal">True</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox4"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">8</property> + <child> + <object class="GtkVBox" id="vbox24"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkHBox" id="hbox13"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkFrame" id="frame-group-1"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkHBox" id="hbox5"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkVBox" id="vbox7"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="checkbutton-group-1"> + <property name="label" translatable="yes">_Show field in View</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox8"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="radiobutton-ascending-group-1"> + <property name="label" translatable="yes">Ascending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radiobutton-descending-group-1"> + <property name="label" translatable="yes">Descending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">radiobutton-ascending-group-1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Group Items By</property> + </object> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox14"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame-group-2"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkHBox" id="hbox6"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkVBox" id="vbox9"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="checkbutton-group-2"> + <property name="label" translatable="yes">Show _field in View</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox10"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="radiobutton-ascending-group-2"> + <property name="label" translatable="yes">Ascending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radiobutton-descending-group-2"> + <property name="label" translatable="yes">Descending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">radiobutton-ascending-group-2</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="label" translatable="yes">Then By</property> + </object> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label19"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label18"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox15"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label32"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame-group-3"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkHBox" id="hbox7"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkVBox" id="vbox11"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="checkbutton-group-3"> + <property name="label" translatable="yes">Show field i_n View</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox12"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="radiobutton-ascending-group-3"> + <property name="label" translatable="yes">Ascending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radiobutton-descending-group-3"> + <property name="label" translatable="yes">Descending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">radiobutton-ascending-group-3</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label20"> + <property name="visible">True</property> + <property name="label" translatable="yes">Then By</property> + </object> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label17"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox16"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label16"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label15"> + <property name="visible">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame-group-4"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkHBox" id="hbox8"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkVBox" id="vbox13"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="checkbutton-group-4"> + <property name="label" translatable="yes">Show field in _View</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox14"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="radiobutton-ascending-group-4"> + <property name="label" translatable="yes">Ascending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radiobutton-descending-group-4"> + <property name="label" translatable="yes">Descending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">radiobutton-ascending-group-4</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label21"> + <property name="visible">True</property> + <property name="label" translatable="yes">Then By</property> + </object> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area4"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button39"> + <property name="label" translatable="yes">Clear _All</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button42"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button41"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button39</action-widget> + <action-widget response="-6">button42</action-widget> + <action-widget response="-5">button41</action-widget> + </action-widgets> + </object> + <object class="GtkDialog" id="dialog-sort"> + <property name="title" translatable="yes">Sort</property> + <property name="modal">True</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="vbox15"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">8</property> + <child> + <object class="GtkTable" id="table5"> + <property name="visible">True</property> + <property name="n_rows">4</property> + <property name="column_spacing">6</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkFrame" id="frame-sort-4"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkHBox" id="hbox9"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="yscale">0</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox17"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="radiobutton-ascending-sort-4"> + <property name="label" translatable="yes">Ascending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radiobutton-descending-sort-4"> + <property name="label" translatable="yes">Descending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">radiobutton-ascending-sort-4</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label22"> + <property name="visible">True</property> + <property name="label" translatable="yes">Then By</property> + </object> + </child> + </object> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame-sort-3"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkHBox" id="hbox10"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="yscale">0</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox19"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="radiobutton-ascending-sort-3"> + <property name="label" translatable="yes">Ascending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radiobutton-descending-sort-3"> + <property name="label" translatable="yes">Descending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">radiobutton-ascending-sort-3</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label23"> + <property name="visible">True</property> + <property name="label" translatable="yes">Then By</property> + </object> + </child> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame-sort-2"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkHBox" id="hbox11"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="yscale">0</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox21"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="radiobutton-ascending-sort-2"> + <property name="label" translatable="yes">Ascending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radiobutton-descending-sort-2"> + <property name="label" translatable="yes">Descending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">radiobutton-ascending-sort-2</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label24"> + <property name="visible">True</property> + <property name="label" translatable="yes">Then By</property> + </object> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame-sort-1"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkHBox" id="hbox12"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="yscale">0</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox23"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="radiobutton-ascending-sort-1"> + <property name="label" translatable="yes">Ascending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radiobutton-descending-sort-1"> + <property name="label" translatable="yes">Descending</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">radiobutton-ascending-sort-1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label25"> + <property name="visible">True</property> + <property name="label" translatable="yes">Sort Items By</property> + </object> + </child> + </object> + <packing> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button43"> + <property name="label" translatable="yes">Clear All</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button45"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button44"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button43</action-widget> + <action-widget response="-6">button45</action-widget> + <action-widget response="-5">button44</action-widget> + </action-widgets> + </object> + <object class="GtkDialog" id="e-table-config"> + <property name="title">dialog1</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="window_position">center-on-parent</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox6"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="top-frame"> + <property name="visible">True</property> + <property name="border_width">2</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="border_width">2</property> + <property name="n_rows">3</property> + <property name="n_columns">3</property> + <property name="column_spacing">4</property> + <property name="row_spacing">2</property> + <child> + <object class="GtkButton" id="button-sort"> + <property name="label" translatable="yes">_Sort...</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_sort_clicked"/> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkButton" id="button-group"> + <property name="label" translatable="yes">_Group By...</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_group_by_clicked"/> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label-sort"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button-fields"> + <property name="label" translatable="yes">_Fields Shown...</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_group_by_clicked"/> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label-fields"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="justify">center</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="justify">center</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label33"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="justify">center</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label-group"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label26"> + <property name="visible">True</property> + <property name="label" translatable="yes">Description</property> + </object> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area6"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="cancelbutton2"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="applybutton2"> + <property name="label">gtk-apply</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="okbutton2"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">cancelbutton2</action-widget> + <action-widget response="-10">applybutton2</action-widget> + <action-widget response="-5">okbutton2</action-widget> + </action-widgets> + </object> +</interface> diff --git a/e-util/e-table-defines.h b/e-util/e-table-defines.h new file mode 100644 index 0000000000..0575f1cea7 --- /dev/null +++ b/e-util/e-table-defines.h @@ -0,0 +1,44 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __E_TABLE_DEFINES__ +#define __E_TABLE_DEFINES__ 1 + +G_BEGIN_DECLS + +#define BUTTON_HEIGHT 10 +#define BUTTON_PADDING 2 +#define GROUP_INDENT (BUTTON_HEIGHT + (BUTTON_PADDING * 2)) + +/* Padding around the contents of a header button */ +#define HEADER_PADDING 3 + +#define MIN_ARROW_SIZE 10 + +G_END_DECLS + +#endif diff --git a/e-util/e-table-extras.c b/e-util/e-table-extras.c new file mode 100644 index 0000000000..1820f35451 --- /dev/null +++ b/e-util/e-table-extras.c @@ -0,0 +1,410 @@ +/* + * e-table-extras.c - Set of hash table sort of thingies. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <string.h> + +#include <gtk/gtk.h> + +#include "e-cell-checkbox.h" +#include "e-cell-date.h" +#include "e-cell-number.h" +#include "e-cell-pixbuf.h" +#include "e-cell-size.h" +#include "e-cell-text.h" +#include "e-cell-tree.h" +#include "e-table-extras.h" +#include "e-table-sorting-utils.h" + +#define E_TABLE_EXTRAS_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TABLE_EXTRAS, ETableExtrasPrivate)) + +struct _ETableExtrasPrivate { + GHashTable *cells; + GHashTable *compares; + GHashTable *icon_names; + GHashTable *searches; +}; + +/* workaround for avoiding API breakage */ +#define ete_get_type e_table_extras_get_type +G_DEFINE_TYPE (ETableExtras, ete, G_TYPE_OBJECT) + +static void +ete_finalize (GObject *object) +{ + ETableExtrasPrivate *priv; + + priv = E_TABLE_EXTRAS_GET_PRIVATE (object); + + if (priv->cells) { + g_hash_table_destroy (priv->cells); + priv->cells = NULL; + } + + if (priv->compares) { + g_hash_table_destroy (priv->compares); + priv->compares = NULL; + } + + if (priv->searches) { + g_hash_table_destroy (priv->searches); + priv->searches = NULL; + } + + if (priv->icon_names) { + g_hash_table_destroy (priv->icon_names); + priv->icon_names = NULL; + } + + G_OBJECT_CLASS (ete_parent_class)->finalize (object); +} + +static void +ete_class_init (ETableExtrasClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ETableExtrasPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = ete_finalize; +} + +static gint +e_strint_compare (gconstpointer data1, + gconstpointer data2) +{ + gint int1 = atoi (data1); + gint int2 = atoi (data2); + + return e_int_compare (GINT_TO_POINTER (int1), GINT_TO_POINTER (int2)); +} + +/* UTF-8 strncasecmp - not optimized */ + +static gint +g_utf8_strncasecmp (const gchar *s1, + const gchar *s2, + guint n) +{ + gunichar c1, c2; + + g_return_val_if_fail (s1 != NULL && g_utf8_validate (s1, -1, NULL), 0); + g_return_val_if_fail (s2 != NULL && g_utf8_validate (s2, -1, NULL), 0); + + while (n && *s1 && *s2) + { + + n -= 1; + + c1 = g_unichar_tolower (g_utf8_get_char (s1)); + c2 = g_unichar_tolower (g_utf8_get_char (s2)); + + /* Collation is locale-dependent, so this + * totally fails to do the right thing. */ + if (c1 != c2) + return c1 < c2 ? -1 : 1; + + s1 = g_utf8_next_char (s1); + s2 = g_utf8_next_char (s2); + } + + if (n == 0 || (*s1 == '\0' && *s2 == '\0')) + return 0; + + return *s1 ? 1 : -1; +} + +static gboolean +e_string_search (gconstpointer haystack, + const gchar *needle) +{ + gint length; + if (haystack == NULL) + return FALSE; + + length = g_utf8_strlen (needle, -1); + if (g_utf8_strncasecmp (haystack, needle, length) == 0) + return TRUE; + else + return FALSE; +} + +static gint +e_table_str_case_compare (gconstpointer x, + gconstpointer y, + gpointer cmp_cache) +{ + const gchar *cx = NULL, *cy = NULL; + + if (!cmp_cache) + return e_str_case_compare (x, y); + + if (x == NULL || y == NULL) { + if (x == y) + return 0; + else + return x ? -1 : 1; + } + + #define prepare_value(_z, _cz) \ + _cz = e_table_sorting_utils_lookup_cmp_cache (cmp_cache, _z); \ + if (!_cz) { \ + gchar *tmp = g_utf8_casefold (_z, -1); \ + _cz = g_utf8_collate_key (tmp, -1); \ + g_free (tmp); \ + \ + e_table_sorting_utils_add_to_cmp_cache ( \ + cmp_cache, _z, (gchar *) _cz); \ + } + + prepare_value (x, cx); + prepare_value (y, cy); + + #undef prepare_value + + return strcmp (cx, cy); +} + +static gint +e_table_collate_compare (gconstpointer x, + gconstpointer y, + gpointer cmp_cache) +{ + const gchar *cx = NULL, *cy = NULL; + + if (!cmp_cache) + return e_collate_compare (x, y); + + if (x == NULL || y == NULL) { + if (x == y) + return 0; + else + return x ? -1 : 1; + } + + #define prepare_value(_z, _cz) \ + _cz = e_table_sorting_utils_lookup_cmp_cache (cmp_cache, _z); \ + if (!_cz) { \ + _cz = g_utf8_collate_key (_z, -1); \ + \ + e_table_sorting_utils_add_to_cmp_cache ( \ + cmp_cache, _z, (gchar *) _cz); \ + } + + prepare_value (x, cx); + prepare_value (y, cy); + + #undef prepare_value + + return strcmp (cx, cy); +} + +static void +safe_unref (gpointer object) +{ + if (object != NULL) + g_object_unref (object); +} + +static void +ete_init (ETableExtras *extras) +{ + ECell *cell, *sub_cell; + + extras->priv = E_TABLE_EXTRAS_GET_PRIVATE (extras); + + extras->priv->cells = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) safe_unref); + + extras->priv->compares = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) NULL); + + extras->priv->icon_names = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + + extras->priv->searches = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) NULL); + + e_table_extras_add_compare ( + extras, "string", + (GCompareDataFunc) e_str_compare); + e_table_extras_add_compare ( + extras, "stringcase", + (GCompareDataFunc) e_table_str_case_compare); + e_table_extras_add_compare ( + extras, "collate", + (GCompareDataFunc) e_table_collate_compare); + e_table_extras_add_compare ( + extras, "integer", + (GCompareDataFunc) e_int_compare); + e_table_extras_add_compare ( + extras, "string-integer", + (GCompareDataFunc) e_strint_compare); + + e_table_extras_add_search (extras, "string", e_string_search); + + cell = e_cell_checkbox_new (); + e_table_extras_add_cell (extras, "checkbox", cell); + g_object_unref (cell); + + cell = e_cell_date_new (NULL, GTK_JUSTIFY_LEFT); + e_table_extras_add_cell (extras, "date", cell); + g_object_unref (cell); + + cell = e_cell_number_new (NULL, GTK_JUSTIFY_RIGHT); + e_table_extras_add_cell (extras, "number", cell); + g_object_unref (cell); + + cell = e_cell_pixbuf_new (); + e_table_extras_add_cell (extras, "pixbuf", cell); + g_object_unref (cell); + + cell = e_cell_size_new (NULL, GTK_JUSTIFY_RIGHT); + e_table_extras_add_cell (extras, "size", cell); + g_object_unref (cell); + + cell = e_cell_text_new (NULL, GTK_JUSTIFY_LEFT); + e_table_extras_add_cell (extras, "string", cell); + g_object_unref (cell); + + sub_cell = e_cell_text_new (NULL, GTK_JUSTIFY_LEFT); + cell = e_cell_tree_new (TRUE, sub_cell); + e_table_extras_add_cell (extras, "tree-string", cell); + g_object_unref (sub_cell); + g_object_unref (cell); +} + +ETableExtras * +e_table_extras_new (void) +{ + return g_object_new (E_TYPE_TABLE_EXTRAS, NULL); +} + +void +e_table_extras_add_cell (ETableExtras *extras, + const gchar *id, + ECell *cell) +{ + g_return_if_fail (E_IS_TABLE_EXTRAS (extras)); + g_return_if_fail (id != NULL); + + if (cell != NULL) + g_object_ref_sink (cell); + + g_hash_table_insert (extras->priv->cells, g_strdup (id), cell); +} + +ECell * +e_table_extras_get_cell (ETableExtras *extras, + const gchar *id) +{ + g_return_val_if_fail (E_IS_TABLE_EXTRAS (extras), NULL); + g_return_val_if_fail (id != NULL, NULL); + + return g_hash_table_lookup (extras->priv->cells, id); +} + +void +e_table_extras_add_compare (ETableExtras *extras, + const gchar *id, + GCompareDataFunc compare) +{ + g_return_if_fail (E_IS_TABLE_EXTRAS (extras)); + g_return_if_fail (id != NULL); + + g_hash_table_insert ( + extras->priv->compares, + g_strdup (id), (gpointer) compare); +} + +GCompareDataFunc +e_table_extras_get_compare (ETableExtras *extras, + const gchar *id) +{ + g_return_val_if_fail (E_IS_TABLE_EXTRAS (extras), NULL); + g_return_val_if_fail (id != NULL, NULL); + + return g_hash_table_lookup (extras->priv->compares, id); +} + +void +e_table_extras_add_search (ETableExtras *extras, + const gchar *id, + ETableSearchFunc search) +{ + g_return_if_fail (E_IS_TABLE_EXTRAS (extras)); + g_return_if_fail (id != NULL); + + g_hash_table_insert ( + extras->priv->searches, + g_strdup (id), (gpointer) search); +} + +ETableSearchFunc +e_table_extras_get_search (ETableExtras *extras, + const gchar *id) +{ + g_return_val_if_fail (E_IS_TABLE_EXTRAS (extras), NULL); + g_return_val_if_fail (id != NULL, NULL); + + return g_hash_table_lookup (extras->priv->searches, id); +} + +void +e_table_extras_add_icon_name (ETableExtras *extras, + const gchar *id, + const gchar *icon_name) +{ + g_return_if_fail (E_IS_TABLE_EXTRAS (extras)); + g_return_if_fail (id != NULL); + + g_hash_table_insert ( + extras->priv->icon_names, + g_strdup (id), g_strdup (icon_name)); +} + +const gchar * +e_table_extras_get_icon_name (ETableExtras *extras, + const gchar *id) +{ + g_return_val_if_fail (E_IS_TABLE_EXTRAS (extras), NULL); + g_return_val_if_fail (id != NULL, NULL); + + return g_hash_table_lookup (extras->priv->icon_names, id); +} diff --git a/e-util/e-table-extras.h b/e-util/e-table-extras.h new file mode 100644 index 0000000000..93acc4cea0 --- /dev/null +++ b/e-util/e-table-extras.h @@ -0,0 +1,94 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_TABLE_EXTRAS_H +#define E_TABLE_EXTRAS_H + +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <e-util/e-cell.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_EXTRAS \ + (e_table_extras_get_type ()) +#define E_TABLE_EXTRAS(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_EXTRAS, ETableExtras)) +#define E_TABLE_EXTRAS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_EXTRAS, ETableExtrasClass)) +#define E_IS_TABLE_EXTRAS(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_EXTRAS)) +#define E_IS_TABLE_EXTRAS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_EXTRAS)) +#define E_TABLE_EXTRAS_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_EXTRAS, ETableExtrasClass)) + +G_BEGIN_DECLS + +typedef struct _ETableExtras ETableExtras; +typedef struct _ETableExtrasClass ETableExtrasClass; +typedef struct _ETableExtrasPrivate ETableExtrasPrivate; + +struct _ETableExtras { + GObject parent; + ETableExtrasPrivate *priv; +}; + +struct _ETableExtrasClass { + GObjectClass parent_class; +}; + +GType e_table_extras_get_type (void) G_GNUC_CONST; +ETableExtras * e_table_extras_new (void); +void e_table_extras_add_cell (ETableExtras *extras, + const gchar *id, + ECell *cell); +ECell * e_table_extras_get_cell (ETableExtras *extras, + const gchar *id); +void e_table_extras_add_compare (ETableExtras *extras, + const gchar *id, + GCompareDataFunc compare); +GCompareDataFunc e_table_extras_get_compare (ETableExtras *extras, + const gchar *id); +void e_table_extras_add_search (ETableExtras *extras, + const gchar *id, + ETableSearchFunc search); +ETableSearchFunc + e_table_extras_get_search (ETableExtras *extras, + const gchar *id); +void e_table_extras_add_icon_name (ETableExtras *extras, + const gchar *id, + const gchar *icon_name); +const gchar * e_table_extras_get_icon_name (ETableExtras *extras, + const gchar *id); + +G_END_DECLS + +#endif /* E_TABLE_EXTRAS_H */ diff --git a/e-util/e-table-field-chooser-dialog.c b/e-util/e-table-field-chooser-dialog.c new file mode 100644 index 0000000000..4c643089a1 --- /dev/null +++ b/e-util/e-table-field-chooser-dialog.c @@ -0,0 +1,235 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> + +#include <glib/gi18n.h> + +#include "e-table-field-chooser-dialog.h" + +enum { + PROP_0, + PROP_DND_CODE, + PROP_FULL_HEADER, + PROP_HEADER +}; + +G_DEFINE_TYPE ( + ETableFieldChooserDialog, + e_table_field_chooser_dialog, + GTK_TYPE_DIALOG) + +static void +e_table_field_chooser_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ETableFieldChooserDialog *etfcd = E_TABLE_FIELD_CHOOSER_DIALOG (object); + switch (property_id) { + case PROP_DND_CODE: + g_free (etfcd->dnd_code); + etfcd->dnd_code = g_strdup (g_value_get_string (value)); + if (etfcd->etfc) + g_object_set ( + etfcd->etfc, + "dnd_code", etfcd->dnd_code, + NULL); + break; + case PROP_FULL_HEADER: + if (etfcd->full_header) + g_object_unref (etfcd->full_header); + if (g_value_get_object (value)) + etfcd->full_header = E_TABLE_HEADER (g_value_get_object (value)); + else + etfcd->full_header = NULL; + if (etfcd->full_header) + g_object_ref (etfcd->full_header); + if (etfcd->etfc) + g_object_set ( + etfcd->etfc, + "full_header", etfcd->full_header, + NULL); + break; + case PROP_HEADER: + if (etfcd->header) + g_object_unref (etfcd->header); + if (g_value_get_object (value)) + etfcd->header = E_TABLE_HEADER (g_value_get_object (value)); + else + etfcd->header = NULL; + if (etfcd->header) + g_object_ref (etfcd->header); + if (etfcd->etfc) + g_object_set ( + etfcd->etfc, + "header", etfcd->header, + NULL); + break; + default: + break; + } +} + +static void +e_table_field_chooser_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableFieldChooserDialog *etfcd = E_TABLE_FIELD_CHOOSER_DIALOG (object); + switch (property_id) { + case PROP_DND_CODE: + g_value_set_string (value, etfcd->dnd_code); + break; + case PROP_FULL_HEADER: + g_value_set_object (value, etfcd->full_header); + break; + case PROP_HEADER: + g_value_set_object (value, etfcd->header); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +e_table_field_chooser_dialog_dispose (GObject *object) +{ + ETableFieldChooserDialog *etfcd = E_TABLE_FIELD_CHOOSER_DIALOG (object); + + if (etfcd->dnd_code) + g_free (etfcd->dnd_code); + etfcd->dnd_code = NULL; + + if (etfcd->full_header) + g_object_unref (etfcd->full_header); + etfcd->full_header = NULL; + + if (etfcd->header) + g_object_unref (etfcd->header); + etfcd->header = NULL; + + G_OBJECT_CLASS (e_table_field_chooser_dialog_parent_class)->dispose (object); +} + +static void +e_table_field_chooser_dialog_response (GtkDialog *dialog, + gint id) +{ + if (id == GTK_RESPONSE_OK) + gtk_widget_destroy (GTK_WIDGET (dialog)); +} + +static void +e_table_field_chooser_dialog_class_init (ETableFieldChooserDialogClass *class) +{ + GObjectClass *object_class; + GtkDialogClass *dialog_class; + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = e_table_field_chooser_dialog_set_property; + object_class->get_property = e_table_field_chooser_dialog_get_property; + object_class->dispose = e_table_field_chooser_dialog_dispose; + + dialog_class = GTK_DIALOG_CLASS (class); + dialog_class->response = e_table_field_chooser_dialog_response; + + g_object_class_install_property ( + object_class, + PROP_DND_CODE, + g_param_spec_string ( + "dnd_code", + "DnD code", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_FULL_HEADER, + g_param_spec_object ( + "full_header", + "Full Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_HEADER, + g_param_spec_object ( + "header", + "Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_READWRITE)); +} + +static void +e_table_field_chooser_dialog_init (ETableFieldChooserDialog *e_table_field_chooser_dialog) +{ + GtkDialog *dialog; + GtkWidget *content_area; + GtkWidget *widget; + + dialog = GTK_DIALOG (e_table_field_chooser_dialog); + + e_table_field_chooser_dialog->etfc = NULL; + e_table_field_chooser_dialog->dnd_code = g_strdup (""); + e_table_field_chooser_dialog->full_header = NULL; + e_table_field_chooser_dialog->header = NULL; + + gtk_dialog_add_button (dialog, GTK_STOCK_CLOSE, GTK_RESPONSE_OK); + + gtk_window_set_resizable (GTK_WINDOW (dialog), TRUE); + + widget = e_table_field_chooser_new (); + e_table_field_chooser_dialog->etfc = E_TABLE_FIELD_CHOOSER (widget); + + g_object_set ( + widget, + "dnd_code", e_table_field_chooser_dialog->dnd_code, + "full_header", e_table_field_chooser_dialog->full_header, + "header", e_table_field_chooser_dialog->header, + NULL); + + content_area = gtk_dialog_get_content_area (dialog); + gtk_box_pack_start (GTK_BOX (content_area), widget, TRUE, TRUE, 0); + + gtk_widget_show (GTK_WIDGET (widget)); + + gtk_window_set_title (GTK_WINDOW (dialog), _("Add a Column")); +} + +GtkWidget * +e_table_field_chooser_dialog_new (void) +{ + return g_object_new (E_TYPE_TABLE_FIELD_CHOOSER_DIALOG, NULL); +} + diff --git a/e-util/e-table-field-chooser-dialog.h b/e-util/e-table-field-chooser-dialog.h new file mode 100644 index 0000000000..15be375c53 --- /dev/null +++ b/e-util/e-table-field-chooser-dialog.h @@ -0,0 +1,77 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __E_TABLE_FIELD_CHOOSER_DIALOG_H__ +#define __E_TABLE_FIELD_CHOOSER_DIALOG_H__ + +#include <gtk/gtk.h> + +#include <e-util/e-table-field-chooser.h> +#include <e-util/e-table-header.h> + +#define E_TYPE_TABLE_FIELD_CHOOSER_DIALOG \ + (e_table_field_chooser_dialog_get_type ()) +#define E_TABLE_FIELD_CHOOSER_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_FIELD_CHOOSER_DIALOG, ETableFieldChooserDialog)) +#define E_TABLE_FIELD_CHOOSER_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_FIELD_CHOOSER_DIALOG, ETableFieldChooserDialogClass)) +#define E_IS_TABLE_FIELD_CHOOSER_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_FIELD_CHOOSER_DIALOG)) +#define E_IS_TABLE_FIELD_CHOOSER_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_FIELD_CHOOSER_DIALOG)) +#define E_TABLE_FIELD_CHOOSER_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_FIELD_CHOOSER_DIALOG, ETableFieldChooserDialogClass)) + +G_BEGIN_DECLS + +typedef struct _ETableFieldChooserDialog ETableFieldChooserDialog; +typedef struct _ETableFieldChooserDialogClass ETableFieldChooserDialogClass; + +struct _ETableFieldChooserDialog { + GtkDialog parent; + + /* item specific fields */ + ETableFieldChooser *etfc; + gchar *dnd_code; + ETableHeader *full_header; + ETableHeader *header; +}; + +struct _ETableFieldChooserDialogClass { + GtkDialogClass parent_class; +}; + +GType e_table_field_chooser_dialog_get_type (void) G_GNUC_CONST; +GtkWidget * e_table_field_chooser_dialog_new (void); + +G_END_DECLS + +#endif /* __E_TABLE_FIELD_CHOOSER_DIALOG_H__ */ diff --git a/e-util/e-table-field-chooser-item.c b/e-util/e-table-field-chooser-item.c new file mode 100644 index 0000000000..f72e059f20 --- /dev/null +++ b/e-util/e-table-field-chooser-item.c @@ -0,0 +1,749 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <libgnomecanvas/libgnomecanvas.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include "e-canvas.h" +#include "e-table-col-dnd.h" +#include "e-table-defines.h" +#include "e-table-field-chooser-item.h" +#include "e-table-header-utils.h" +#include "e-table-header.h" +#include "e-xml-utils.h" + +#define d(x) + +#if 0 +enum { + BUTTON_PRESSED, + LAST_SIGNAL +}; + +static guint etfci_signals[LAST_SIGNAL] = { 0, }; +#endif + +/* workaround for avoiding API breakage */ +#define etfci_get_type e_table_field_chooser_item_get_type +G_DEFINE_TYPE (ETableFieldChooserItem, etfci, GNOME_TYPE_CANVAS_ITEM) + +static void etfci_drop_table_header (ETableFieldChooserItem *etfci); +static void etfci_drop_full_header (ETableFieldChooserItem *etfci); + +enum { + PROP_0, + PROP_FULL_HEADER, + PROP_HEADER, + PROP_DND_CODE, + PROP_WIDTH, + PROP_HEIGHT +}; + +static void +etfci_dispose (GObject *object) +{ + ETableFieldChooserItem *etfci = E_TABLE_FIELD_CHOOSER_ITEM (object); + + etfci_drop_table_header (etfci); + etfci_drop_full_header (etfci); + + if (etfci->combined_header) + g_object_unref (etfci->combined_header); + etfci->combined_header = NULL; + + if (etfci->font_desc) + pango_font_description_free (etfci->font_desc); + etfci->font_desc = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (etfci_parent_class)->dispose (object); +} + +static gint +etfci_find_button (ETableFieldChooserItem *etfci, + gdouble loc) +{ + gint i; + gint count; + gdouble height = 0; + + count = e_table_header_count (etfci->combined_header); + for (i = 0; i < count; i++) { + ETableCol *ecol; + + ecol = e_table_header_get_column (etfci->combined_header, i); + if (ecol->disabled) + continue; + height += e_table_header_compute_height ( + ecol, GTK_WIDGET (GNOME_CANVAS_ITEM (etfci)->canvas)); + if (height > loc) + return i; + } + return MAX (0, count - 1); +} + +static void +etfci_rebuild_combined (ETableFieldChooserItem *etfci) +{ + gint count; + GHashTable *hash; + gint i; + + if (etfci->combined_header != NULL) + g_object_unref (etfci->combined_header); + + etfci->combined_header = e_table_header_new (); + + hash = g_hash_table_new (NULL, NULL); + + count = e_table_header_count (etfci->header); + for (i = 0; i < count; i++) { + ETableCol *ecol = e_table_header_get_column (etfci->header, i); + if (ecol->disabled) + continue; + g_hash_table_insert ( + hash, GINT_TO_POINTER (ecol->col_idx), + GINT_TO_POINTER (1)); + } + + count = e_table_header_count (etfci->full_header); + for (i = 0; i < count; i++) { + ETableCol *ecol = e_table_header_get_column (etfci->full_header, i); + if (ecol->disabled) + continue; + if (!(GPOINTER_TO_INT (g_hash_table_lookup ( + hash, GINT_TO_POINTER (ecol->col_idx))))) + e_table_header_add_column (etfci->combined_header, ecol, -1); + } + + g_hash_table_destroy (hash); +} + +static void +etfci_reflow (GnomeCanvasItem *item, + gint flags) +{ + ETableFieldChooserItem *etfci = E_TABLE_FIELD_CHOOSER_ITEM (item); + gdouble old_height; + gint i; + gint count; + gdouble height = 0; + + etfci_rebuild_combined (etfci); + + old_height = etfci->height; + + count = e_table_header_count (etfci->combined_header); + for (i = 0; i < count; i++) { + ETableCol *ecol; + + ecol = e_table_header_get_column (etfci->combined_header, i); + if (ecol->disabled) + continue; + height += e_table_header_compute_height ( + ecol, GTK_WIDGET (GNOME_CANVAS_ITEM (etfci)->canvas)); + } + + etfci->height = height; + + if (old_height != etfci->height) + e_canvas_item_request_parent_reflow (item); + + gnome_canvas_item_request_update (item); +} + +static void +etfci_update (GnomeCanvasItem *item, + const cairo_matrix_t *i2c, + gint flags) +{ + ETableFieldChooserItem *etfci = E_TABLE_FIELD_CHOOSER_ITEM (item); + gdouble x1, y1, x2, y2; + + if (GNOME_CANVAS_ITEM_CLASS (etfci_parent_class)->update) + GNOME_CANVAS_ITEM_CLASS (etfci_parent_class)->update ( + item, i2c, flags); + + x1 = y1 = 0; + x2 = etfci->width; + y2 = etfci->height; + + gnome_canvas_matrix_transform_rect (i2c, &x1, &y1, &x2, &y2); + + if (item->x1 != x1 || + item->y1 != y1 || + item->x2 != x2 || + item->y2 != y2) + { + gnome_canvas_request_redraw ( + item->canvas, item->x1, + item->y1, item->x2, item->y2); + item->x1 = x1; + item->y1 = y1; + item->x2 = x2; + item->y2 = y2; +/* FIXME: Group Child bounds !? */ +#if 0 + gnome_canvas_group_child_bounds ( + GNOME_CANVAS_GROUP (item->parent), item); +#endif + } + gnome_canvas_request_redraw ( + item->canvas, item->x1, item->y1, item->x2, item->y2); +} + +static void +etfci_font_load (ETableFieldChooserItem *etfci) +{ + GtkWidget *widget; + GtkStyle *style; + + if (etfci->font_desc) + pango_font_description_free (etfci->font_desc); + + widget = GTK_WIDGET (GNOME_CANVAS_ITEM (etfci)->canvas); + style = gtk_widget_get_style (widget); + etfci->font_desc = pango_font_description_copy (style->font_desc); +} + +static void +etfci_drop_full_header (ETableFieldChooserItem *etfci) +{ + GObject *header; + + if (!etfci->full_header) + return; + + header = G_OBJECT (etfci->full_header); + if (etfci->full_header_structure_change_id) + g_signal_handler_disconnect ( + header, etfci->full_header_structure_change_id); + if (etfci->full_header_dimension_change_id) + g_signal_handler_disconnect ( + header, etfci->full_header_dimension_change_id); + etfci->full_header_structure_change_id = 0; + etfci->full_header_dimension_change_id = 0; + + if (header) + g_object_unref (header); + etfci->full_header = NULL; + etfci->height = 0; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etfci)); +} + +static void +full_header_structure_changed (ETableHeader *header, + ETableFieldChooserItem *etfci) +{ + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etfci)); +} + +static void +full_header_dimension_changed (ETableHeader *header, + gint col, + ETableFieldChooserItem *etfci) +{ + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etfci)); +} + +static void +etfci_add_full_header (ETableFieldChooserItem *etfci, + ETableHeader *header) +{ + etfci->full_header = header; + g_object_ref (etfci->full_header); + + etfci->full_header_structure_change_id = g_signal_connect ( + header, "structure_change", + G_CALLBACK (full_header_structure_changed), etfci); + etfci->full_header_dimension_change_id = g_signal_connect ( + header, "dimension_change", + G_CALLBACK (full_header_dimension_changed), etfci); + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etfci)); +} + +static void +etfci_drop_table_header (ETableFieldChooserItem *etfci) +{ + GObject *header; + + if (!etfci->header) + return; + + header = G_OBJECT (etfci->header); + if (etfci->table_header_structure_change_id) + g_signal_handler_disconnect ( + header, etfci->table_header_structure_change_id); + if (etfci->table_header_dimension_change_id) + g_signal_handler_disconnect ( + header, etfci->table_header_dimension_change_id); + etfci->table_header_structure_change_id = 0; + etfci->table_header_dimension_change_id = 0; + + if (header) + g_object_unref (header); + etfci->header = NULL; + etfci->height = 0; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etfci)); +} + +static void +table_header_structure_changed (ETableHeader *header, + ETableFieldChooserItem *etfci) +{ + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etfci)); +} + +static void +table_header_dimension_changed (ETableHeader *header, + gint col, + ETableFieldChooserItem *etfci) +{ + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etfci)); +} + +static void +etfci_add_table_header (ETableFieldChooserItem *etfci, + ETableHeader *header) +{ + etfci->header = header; + g_object_ref (etfci->header); + + etfci->table_header_structure_change_id = g_signal_connect ( + header, "structure_change", + G_CALLBACK (table_header_structure_changed), etfci); + etfci->table_header_dimension_change_id = g_signal_connect ( + header, "dimension_change", + G_CALLBACK (table_header_dimension_changed), etfci); + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etfci)); +} + +static void +etfci_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GnomeCanvasItem *item; + ETableFieldChooserItem *etfci; + + item = GNOME_CANVAS_ITEM (object); + etfci = E_TABLE_FIELD_CHOOSER_ITEM (object); + + switch (property_id) { + case PROP_FULL_HEADER: + etfci_drop_full_header (etfci); + if (g_value_get_object (value)) + etfci_add_full_header ( + etfci, E_TABLE_HEADER ( + g_value_get_object (value))); + break; + + case PROP_HEADER: + etfci_drop_table_header (etfci); + if (g_value_get_object (value)) + etfci_add_table_header ( + etfci, E_TABLE_HEADER ( + g_value_get_object (value))); + break; + + case PROP_DND_CODE: + g_free (etfci->dnd_code); + etfci->dnd_code = g_strdup (g_value_get_string (value)); + break; + + case PROP_WIDTH: + etfci->width = g_value_get_double (value); + gnome_canvas_item_request_update (item); + break; + } +} + +static void +etfci_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableFieldChooserItem *etfci; + + etfci = E_TABLE_FIELD_CHOOSER_ITEM (object); + + switch (property_id) { + + case PROP_DND_CODE: + g_value_set_string (value, etfci->dnd_code); + break; + case PROP_WIDTH: + g_value_set_double (value, etfci->width); + break; + case PROP_HEIGHT: + g_value_set_double (value, etfci->height); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +etfci_drag_data_get (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time, + ETableFieldChooserItem *etfci) +{ + if (etfci->drag_col != -1) { + gchar *string = g_strdup_printf ("%d", etfci->drag_col); + gtk_selection_data_set ( + selection_data, + GDK_SELECTION_TYPE_STRING, + sizeof (string[0]), + (guchar *) string, + strlen (string)); + g_free (string); + } +} + +static void +etfci_drag_end (GtkWidget *canvas, + GdkDragContext *context, + ETableFieldChooserItem *etfci) +{ + etfci->drag_col = -1; +} + +static void +etfci_realize (GnomeCanvasItem *item) +{ + ETableFieldChooserItem *etfci = E_TABLE_FIELD_CHOOSER_ITEM (item); + + if (GNOME_CANVAS_ITEM_CLASS (etfci_parent_class)-> realize) + (*GNOME_CANVAS_ITEM_CLASS (etfci_parent_class)->realize)(item); + + if (!etfci->font_desc) + etfci_font_load (etfci); + + etfci->drag_end_id = g_signal_connect ( + item->canvas, "drag_end", + G_CALLBACK (etfci_drag_end), etfci); + etfci->drag_data_get_id = g_signal_connect ( + item->canvas, "drag_data_get", + G_CALLBACK (etfci_drag_data_get), etfci); + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etfci)); +} + +static void +etfci_unrealize (GnomeCanvasItem *item) +{ + ETableFieldChooserItem *etfci = E_TABLE_FIELD_CHOOSER_ITEM (item); + + if (etfci->font_desc) + pango_font_description_free (etfci->font_desc); + etfci->font_desc = NULL; + + g_signal_handler_disconnect (item->canvas, etfci->drag_end_id); + etfci->drag_end_id = 0; + g_signal_handler_disconnect (item->canvas, etfci->drag_data_get_id); + etfci->drag_data_get_id = 0; + + if (GNOME_CANVAS_ITEM_CLASS (etfci_parent_class)->unrealize) + (*GNOME_CANVAS_ITEM_CLASS (etfci_parent_class)->unrealize)(item); +} + +static void +etfci_draw (GnomeCanvasItem *item, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height) +{ + ETableFieldChooserItem *etfci = E_TABLE_FIELD_CHOOSER_ITEM (item); + GnomeCanvas *canvas = item->canvas; + gint rows; + gint y1, y2; + gint row; + + if (etfci->combined_header == NULL) + return; + + rows = e_table_header_count (etfci->combined_header); + + y1 = y2 = 0; + for (row = 0; row < rows; row++, y1 = y2) { + ETableCol *ecol; + + ecol = e_table_header_get_column (etfci->combined_header, row); + + if (ecol->disabled) + continue; + + y2 += e_table_header_compute_height (ecol, GTK_WIDGET (canvas)); + + if (y1 > (y + height)) + break; + + if (y2 < y) + continue; + + cairo_save (cr); + + e_table_header_draw_button ( + cr, ecol, + GTK_WIDGET (canvas), + -x, y1 - y, + width, height, + etfci->width, y2 - y1, + E_TABLE_COL_ARROW_NONE); + + cairo_restore (cr); + } +} + +static GnomeCanvasItem * +etfci_point (GnomeCanvasItem *item, + gdouble x, + gdouble y, + gint cx, + gint cy) +{ + return item; +} + +static gboolean +etfci_maybe_start_drag (ETableFieldChooserItem *etfci, + gint x, + gint y) +{ + if (!etfci->maybe_drag) + return FALSE; + + if (MAX (abs (etfci->click_x - x), + abs (etfci->click_y - y)) <= 3) + return FALSE; + + return TRUE; +} + +static void +etfci_start_drag (ETableFieldChooserItem *etfci, + GdkEvent *event, + gdouble x, + gdouble y) +{ + GtkWidget *widget = GTK_WIDGET (GNOME_CANVAS_ITEM (etfci)->canvas); + GtkTargetList *list; + GdkDragContext *context; + ETableCol *ecol; + cairo_surface_t *cs; + cairo_t *cr; + gint drag_col; + gint button_height; + + GtkTargetEntry etfci_drag_types[] = { + { (gchar *) TARGET_ETABLE_COL_TYPE, 0, TARGET_ETABLE_COL_HEADER }, + }; + + if (etfci->combined_header == NULL) + return; + + drag_col = etfci_find_button (etfci, y); + + if (drag_col < 0 || drag_col > e_table_header_count (etfci->combined_header)) + return; + + ecol = e_table_header_get_column (etfci->combined_header, drag_col); + + if (ecol->disabled) + return; + + etfci->drag_col = ecol->col_idx; + + etfci_drag_types[0].target = g_strdup_printf ( + "%s-%s", etfci_drag_types[0].target, etfci->dnd_code); + d (g_print ("etfci - %s\n", etfci_drag_types[0].target)); + list = gtk_target_list_new (etfci_drag_types, G_N_ELEMENTS (etfci_drag_types)); + context = gtk_drag_begin (widget, list, GDK_ACTION_MOVE, 1, event); + g_free ((gpointer) etfci_drag_types[0].target); + + button_height = e_table_header_compute_height (ecol, widget); + cs = cairo_image_surface_create ( + CAIRO_FORMAT_ARGB32, + etfci->width, button_height); + cr = cairo_create (cs); + + e_table_header_draw_button ( + cr, ecol, + widget, 0, 0, + etfci->width, button_height, + etfci->width, button_height, + E_TABLE_COL_ARROW_NONE); + + gtk_drag_set_icon_surface (context, cs); + + cairo_surface_destroy (cs); + cairo_destroy (cr); + etfci->maybe_drag = FALSE; +} + +/* + * Handles the events on the ETableFieldChooserItem + */ +static gint +etfci_event (GnomeCanvasItem *item, + GdkEvent *e) +{ + ETableFieldChooserItem *etfci = E_TABLE_FIELD_CHOOSER_ITEM (item); + GnomeCanvas *canvas = item->canvas; + gint x, y; + + switch (e->type) { + case GDK_MOTION_NOTIFY: + gnome_canvas_w2c (canvas, e->motion.x, e->motion.y, &x, &y); + + if (etfci_maybe_start_drag (etfci, x, y)) + etfci_start_drag (etfci, e, x, y); + break; + + case GDK_BUTTON_PRESS: + gnome_canvas_w2c (canvas, e->button.x, e->button.y, &x, &y); + + if (e->button.button == 1) { + etfci->click_x = x; + etfci->click_y = y; + etfci->maybe_drag = TRUE; + } + break; + + case GDK_BUTTON_RELEASE: { + etfci->maybe_drag = FALSE; + break; + } + + default: + return FALSE; + } + return TRUE; +} + +static void +etfci_class_init (ETableFieldChooserItemClass *class) +{ + GnomeCanvasItemClass *item_class = GNOME_CANVAS_ITEM_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = etfci_dispose; + object_class->set_property = etfci_set_property; + object_class->get_property = etfci_get_property; + + item_class->update = etfci_update; + item_class->realize = etfci_realize; + item_class->unrealize = etfci_unrealize; + item_class->draw = etfci_draw; + item_class->point = etfci_point; + item_class->event = etfci_event; + + g_object_class_install_property ( + object_class, + PROP_DND_CODE, + g_param_spec_string ( + "dnd_code", + "DnD code", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_FULL_HEADER, + g_param_spec_object ( + "full_header", + "Full Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_HEADER, + g_param_spec_object ( + "header", + "Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WIDTH, + g_param_spec_double ( + "width", + "Width", + NULL, + 0, G_MAXDOUBLE, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_HEIGHT, + g_param_spec_double ( + "height", + "Height", + NULL, + 0, G_MAXDOUBLE, 0, + G_PARAM_READABLE)); +} + +static void +etfci_init (ETableFieldChooserItem *etfci) +{ + etfci->full_header = NULL; + etfci->header = NULL; + etfci->combined_header = NULL; + + etfci->height = etfci->width = 0; + + etfci->font_desc = NULL; + + etfci->full_header_structure_change_id = 0; + etfci->full_header_dimension_change_id = 0; + etfci->table_header_structure_change_id = 0; + etfci->table_header_dimension_change_id = 0; + + etfci->dnd_code = NULL; + + etfci->maybe_drag = 0; + etfci->drag_end_id = 0; + + e_canvas_item_set_reflow_callback (GNOME_CANVAS_ITEM (etfci), etfci_reflow); +} + diff --git a/e-util/e-table-field-chooser-item.h b/e-util/e-table-field-chooser-item.h new file mode 100644 index 0000000000..08bfeb6729 --- /dev/null +++ b/e-util/e-table-field-chooser-item.h @@ -0,0 +1,97 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_FIELD_CHOOSER_ITEM_H_ +#define _E_TABLE_FIELD_CHOOSER_ITEM_H_ + +#include <libgnomecanvas/libgnomecanvas.h> +#include <libxml/tree.h> + +#include <e-util/e-table-header.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_FIELD_CHOOSER_ITEM \ + (e_table_field_chooser_item_get_type ()) +#define E_TABLE_FIELD_CHOOSER_ITEM(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_FIELD_CHOOSER_ITEM, ETableFieldChooserItem)) +#define E_TABLE_FIELD_CHOOSER_ITEM_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_FIELD_CHOOSER_ITEM, ETableFieldChooserItemClass)) +#define E_IS_TABLE_FIELD_CHOOSER_ITEM(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_FIELD_CHOOSER_ITEM)) +#define E_IS_TABLE_FIELD_CHOOSER_ITEM_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_FIELD_CHOOSER_ITEM)) +#define E_TABLE_FIELD_CHOOSER_ITEM_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_FIELD_CHOOSER_ITEM, ETableFieldChooserItemClass)) + +G_BEGIN_DECLS + +typedef struct _ETableFieldChooserItem ETableFieldChooserItem; +typedef struct _ETableFieldChooserItemClass ETableFieldChooserItemClass; + +struct _ETableFieldChooserItem { + GnomeCanvasItem parent; + + ETableHeader *full_header; + ETableHeader *header; + ETableHeader *combined_header; + + gdouble height, width; + + PangoFontDescription *font_desc; + + /* + * Ids + */ + gint full_header_structure_change_id, full_header_dimension_change_id; + gint table_header_structure_change_id, table_header_dimension_change_id; + + gchar *dnd_code; + + /* + * For dragging columns + */ + guint maybe_drag : 1; + gint click_x, click_y; + gint drag_col; + guint drag_data_get_id; + guint drag_end_id; +}; + +struct _ETableFieldChooserItemClass { + GnomeCanvasItemClass parent_class; +}; + +GType e_table_field_chooser_item_get_type (void) G_GNUC_CONST; + +G_END_DECLS + +#endif /* _E_TABLE_FIELD_CHOOSER_ITEM_H_ */ diff --git a/e-util/e-table-field-chooser.c b/e-util/e-table-field-chooser.c new file mode 100644 index 0000000000..c402edb7fe --- /dev/null +++ b/e-util/e-table-field-chooser.c @@ -0,0 +1,335 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-field-chooser.h" + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-canvas.h" +#include "e-table-field-chooser-item.h" +#include "e-util-private.h" + +static void e_table_field_chooser_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); +static void e_table_field_chooser_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); +static void e_table_field_chooser_dispose (GObject *object); + +enum { + PROP_0, + PROP_FULL_HEADER, + PROP_HEADER, + PROP_DND_CODE +}; + +G_DEFINE_TYPE (ETableFieldChooser, e_table_field_chooser, GTK_TYPE_VBOX) + +static void +e_table_field_chooser_class_init (ETableFieldChooserClass *class) +{ + GObjectClass *object_class; + + object_class = (GObjectClass *) class; + + object_class->set_property = e_table_field_chooser_set_property; + object_class->get_property = e_table_field_chooser_get_property; + object_class->dispose = e_table_field_chooser_dispose; + + g_object_class_install_property ( + object_class, + PROP_DND_CODE, + g_param_spec_string ( + "dnd_code", + "DnD code", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_FULL_HEADER, + g_param_spec_object ( + "full_header", + "Full Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_HEADER, + g_param_spec_object ( + "header", + "Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_READWRITE)); +} + +static void +ensure_nonzero_step_increments (ETableFieldChooser *etfc) +{ + GtkAdjustment *va, *ha; + + va = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (etfc->canvas)); + ha = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (etfc->canvas)); + + /* + it looks pretty complicated to get height of column header + so use 16 pixels which should be OK + */ + if (va) + gtk_adjustment_set_step_increment (va, 16.0); + if (ha) + gtk_adjustment_set_step_increment (ha, 16.0); +} + +static void allocate_callback (GtkWidget *canvas, GtkAllocation *allocation, ETableFieldChooser *etfc) +{ + gdouble height; + etfc->last_alloc = *allocation; + gnome_canvas_item_set ( + etfc->item, + "width", (gdouble) allocation->width, + NULL); + g_object_get ( + etfc->item, + "height", &height, + NULL); + height = MAX (height, allocation->height); + gnome_canvas_set_scroll_region (GNOME_CANVAS (etfc->canvas), 0, 0, allocation->width - 1, height - 1); + gnome_canvas_item_set ( + etfc->rect, + "x2", (gdouble) allocation->width, + "y2", (gdouble) height, + NULL); + ensure_nonzero_step_increments (etfc); +} + +static void resize (GnomeCanvas *canvas, ETableFieldChooser *etfc) +{ + gdouble height; + g_object_get ( + etfc->item, + "height", &height, + NULL); + + height = MAX (height, etfc->last_alloc.height); + + gnome_canvas_set_scroll_region (GNOME_CANVAS (etfc->canvas), 0, 0, etfc->last_alloc.width - 1, height - 1); + gnome_canvas_item_set ( + etfc->rect, + "x2", (gdouble) etfc->last_alloc.width, + "y2", (gdouble) height, + NULL); + ensure_nonzero_step_increments (etfc); +} + +static GtkWidget * +create_content (GnomeCanvas **canvas) +{ + GtkWidget *vbox_top; + GtkWidget *label1; + GtkWidget *scrolledwindow1; + GtkWidget *canvas_buttons; + + g_return_val_if_fail (canvas != NULL, NULL); + + vbox_top = gtk_vbox_new (FALSE, 4); + gtk_widget_show (vbox_top); + + label1 = gtk_label_new (_("To add a column to your table, drag it into\nthe location in which you want it to appear.")); + gtk_widget_show (label1); + gtk_box_pack_start (GTK_BOX (vbox_top), label1, FALSE, FALSE, 0); + gtk_label_set_justify (GTK_LABEL (label1), GTK_JUSTIFY_CENTER); + + scrolledwindow1 = gtk_scrolled_window_new (NULL, NULL); + gtk_widget_show (scrolledwindow1); + gtk_box_pack_start (GTK_BOX (vbox_top), scrolledwindow1, TRUE, TRUE, 0); + gtk_widget_set_can_focus (scrolledwindow1, FALSE); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwindow1), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + + canvas_buttons = e_canvas_new (); + gtk_widget_show (canvas_buttons); + gtk_container_add (GTK_CONTAINER (scrolledwindow1), canvas_buttons); + gtk_widget_set_can_focus (canvas_buttons, FALSE); + gtk_widget_set_can_default (canvas_buttons, FALSE); + + *canvas = GNOME_CANVAS (canvas_buttons); + + return vbox_top; +} + +static void +e_table_field_chooser_init (ETableFieldChooser *etfc) +{ + GtkWidget *widget; + + widget = create_content (&etfc->canvas); + if (!widget) { + return; + } + + gtk_widget_set_size_request (widget, -1, 250); + gtk_box_pack_start (GTK_BOX (etfc), widget, TRUE, TRUE, 0); + + etfc->rect = gnome_canvas_item_new ( + gnome_canvas_root (GNOME_CANVAS (etfc->canvas)), + gnome_canvas_rect_get_type (), + "x1", (gdouble) 0, + "y1", (gdouble) 0, + "x2", (gdouble) 100, + "y2", (gdouble) 100, + "fill_color", "white", + NULL); + + etfc->item = gnome_canvas_item_new ( + gnome_canvas_root (etfc->canvas), + e_table_field_chooser_item_get_type (), + "width", (gdouble) 100, + "full_header", etfc->full_header, + "header", etfc->header, + "dnd_code", etfc->dnd_code, + NULL); + + g_signal_connect ( + etfc->canvas, "reflow", + G_CALLBACK (resize), etfc); + + gnome_canvas_set_scroll_region ( + GNOME_CANVAS (etfc->canvas), + 0, 0, 100, 100); + + /* Connect the signals */ + g_signal_connect ( + etfc->canvas, "size_allocate", + G_CALLBACK (allocate_callback), etfc); + + gtk_widget_show_all (widget); +} + +static void +e_table_field_chooser_dispose (GObject *object) +{ + ETableFieldChooser *etfc = E_TABLE_FIELD_CHOOSER (object); + + g_free (etfc->dnd_code); + etfc->dnd_code = NULL; + + if (etfc->full_header) + g_object_unref (etfc->full_header); + etfc->full_header = NULL; + + if (etfc->header) + g_object_unref (etfc->header); + etfc->header = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_table_field_chooser_parent_class)->dispose (object); +} + +GtkWidget * +e_table_field_chooser_new (void) +{ + return g_object_new (E_TYPE_TABLE_FIELD_CHOOSER, NULL); +} + +static void +e_table_field_chooser_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ETableFieldChooser *etfc = E_TABLE_FIELD_CHOOSER (object); + + switch (property_id) { + case PROP_DND_CODE: + g_free (etfc->dnd_code); + etfc->dnd_code = g_strdup (g_value_get_string (value)); + if (etfc->item) + g_object_set ( + etfc->item, + "dnd_code", etfc->dnd_code, + NULL); + break; + case PROP_FULL_HEADER: + if (etfc->full_header) + g_object_unref (etfc->full_header); + if (g_value_get_object (value)) + etfc->full_header = E_TABLE_HEADER (g_value_get_object (value)); + else + etfc->full_header = NULL; + if (etfc->full_header) + g_object_ref (etfc->full_header); + if (etfc->item) + g_object_set ( + etfc->item, + "full_header", etfc->full_header, + NULL); + break; + case PROP_HEADER: + if (etfc->header) + g_object_unref (etfc->header); + if (g_value_get_object (value)) + etfc->header = E_TABLE_HEADER (g_value_get_object (value)); + else + etfc->header = NULL; + if (etfc->header) + g_object_ref (etfc->header); + if (etfc->item) + g_object_set ( + etfc->item, + "header", etfc->header, + NULL); + break; + default: + break; + } +} + +static void +e_table_field_chooser_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableFieldChooser *etfc = E_TABLE_FIELD_CHOOSER (object); + + switch (property_id) { + case PROP_DND_CODE: + g_value_set_string (value, etfc->dnd_code); + break; + case PROP_FULL_HEADER: + g_value_set_object (value, etfc->full_header); + break; + case PROP_HEADER: + g_value_set_object (value, etfc->header); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} diff --git a/e-util/e-table-field-chooser.h b/e-util/e-table-field-chooser.h new file mode 100644 index 0000000000..567b9afa5c --- /dev/null +++ b/e-util/e-table-field-chooser.h @@ -0,0 +1,84 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __E_TABLE_FIELD_CHOOSER_H__ +#define __E_TABLE_FIELD_CHOOSER_H__ + +#include <gtk/gtk.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-table-header.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_FIELD_CHOOSER \ + (e_table_field_chooser_get_type ()) +#define E_TABLE_FIELD_CHOOSER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_FIELD_CHOOSER, ETableFieldChooser)) +#define E_TABLE_FIELD_CHOOSER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_FIELD_CHOOSER, ETableFieldChooserClass)) +#define E_IS_TABLE_FIELD_CHOOSER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_FIELD_CHOOSER)) +#define E_IS_TABLE_FIELD_CHOOSER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_FIELD_CHOOSER)) +#define E_TABLE_FIELD_CHOOSER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_FIELD_CHOOSER, ETableFieldChooserClass)) + +G_BEGIN_DECLS + +typedef struct _ETableFieldChooser ETableFieldChooser; +typedef struct _ETableFieldChooserClass ETableFieldChooserClass; + +struct _ETableFieldChooser { + GtkBox parent; + + /* item specific fields */ + GnomeCanvas *canvas; + GnomeCanvasItem *item; + + GnomeCanvasItem *rect; + GtkAllocation last_alloc; + + gchar *dnd_code; + ETableHeader *full_header; + ETableHeader *header; +}; + +struct _ETableFieldChooserClass { + GtkBoxClass parent_class; +}; + +GType e_table_field_chooser_get_type (void) G_GNUC_CONST; +GtkWidget * e_table_field_chooser_new (void); + +G_END_DECLS + +#endif /* __E_TABLE_FIELD_CHOOSER_H__ */ diff --git a/e-util/e-table-group-container.c b/e-util/e-table-group-container.c new file mode 100644 index 0000000000..5741cd1093 --- /dev/null +++ b/e-util/e-table-group-container.c @@ -0,0 +1,1667 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-group-container.h" + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-canvas-utils.h" +#include "e-canvas.h" +#include "e-table-defines.h" +#include "e-table-group-leaf.h" +#include "e-table-item.h" +#include "e-table-sorting-utils.h" +#include "e-text.h" +#include "e-unicode.h" + +#define TITLE_HEIGHT 16 + +/* workaround for avoiding API breakage */ +#define etgc_get_type e_table_group_container_get_type +G_DEFINE_TYPE (ETableGroupContainer, etgc, E_TYPE_TABLE_GROUP) + +enum { + PROP_0, + PROP_HEIGHT, + PROP_WIDTH, + PROP_MINIMUM_WIDTH, + PROP_FROZEN, + PROP_TABLE_ALTERNATING_ROW_COLORS, + PROP_TABLE_HORIZONTAL_DRAW_GRID, + PROP_TABLE_VERTICAL_DRAW_GRID, + PROP_TABLE_DRAW_FOCUS, + PROP_CURSOR_MODE, + PROP_SELECTION_MODEL, + PROP_LENGTH_THRESHOLD, + PROP_UNIFORM_ROW_HEIGHT +}; + +static EPrintable * +etgc_get_printable (ETableGroup *etg); + +static void +e_table_group_container_child_node_free (ETableGroupContainer *etgc, + ETableGroupContainerChildNode *child_node) +{ + ETableGroup *etg = E_TABLE_GROUP (etgc); + ETableGroup *child = child_node->child; + + g_object_run_dispose (G_OBJECT (child)); + e_table_model_free_value ( + etg->model, etgc->ecol->col_idx, + child_node->key); + g_free (child_node->string); + g_object_run_dispose (G_OBJECT (child_node->text)); + g_object_run_dispose (G_OBJECT (child_node->rect)); +} + +static void +e_table_group_container_list_free (ETableGroupContainer *etgc) +{ + ETableGroupContainerChildNode *child_node; + GList *list; + + for (list = etgc->children; list; list = g_list_next (list)) { + child_node = (ETableGroupContainerChildNode *) list->data; + e_table_group_container_child_node_free (etgc, child_node); + } + + g_list_free (etgc->children); + etgc->children = NULL; +} + +static void +etgc_dispose (GObject *object) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (object); + + if (etgc->children) + e_table_group_container_list_free (etgc); + + if (etgc->font_desc) + pango_font_description_free (etgc->font_desc); + etgc->font_desc = NULL; + + if (etgc->ecol) + g_object_unref (etgc->ecol); + etgc->ecol = NULL; + + if (etgc->sort_info) + g_object_unref (etgc->sort_info); + etgc->sort_info = NULL; + + if (etgc->selection_model) + g_object_unref (etgc->selection_model); + etgc->selection_model = NULL; + + if (etgc->rect) + g_object_run_dispose (G_OBJECT (etgc->rect)); + etgc->rect = NULL; + + G_OBJECT_CLASS (etgc_parent_class)->dispose (object); +} + +/** + * e_table_group_container_construct + * @parent: The %GnomeCanvasGroup to create a child of. + * @etgc: The %ETableGroupContainer. + * @full_header: The full header of the %ETable. + * @header: The current header of the %ETable. + * @model: The %ETableModel of the %ETable. + * @sort_info: The %ETableSortInfo of the %ETable. + * @n: Which grouping level this is (Starts at 0 and sends n + 1 to any child %ETableGroups. + * + * This routine constructs the new %ETableGroupContainer. + */ +void +e_table_group_container_construct (GnomeCanvasGroup *parent, + ETableGroupContainer *etgc, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model, + ETableSortInfo *sort_info, + gint n) +{ + ETableCol *col; + ETableSortColumn column = e_table_sort_info_grouping_get_nth (sort_info, n); + GtkWidget *widget; + GtkStyle *style; + + col = e_table_header_get_column_by_col_idx (full_header, column.column); + if (col == NULL) + col = e_table_header_get_column (full_header, e_table_header_count (full_header) - 1); + + e_table_group_construct (parent, E_TABLE_GROUP (etgc), full_header, header, model); + etgc->ecol = col; + g_object_ref (etgc->ecol); + etgc->sort_info = sort_info; + g_object_ref (etgc->sort_info); + etgc->n = n; + etgc->ascending = column.ascending; + + widget = GTK_WIDGET (GNOME_CANVAS_ITEM (etgc)->canvas); + style = gtk_widget_get_style (widget); + etgc->font_desc = pango_font_description_copy (style->font_desc); + + etgc->open = TRUE; +} + +/** + * e_table_group_container_new + * @parent: The %GnomeCanvasGroup to create a child of. + * @full_header: The full header of the %ETable. + * @header: The current header of the %ETable. + * @model: The %ETableModel of the %ETable. + * @sort_info: The %ETableSortInfo of the %ETable. + * @n: Which grouping level this is (Starts at 0 and sends n + 1 to any child %ETableGroups. + * + * %ETableGroupContainer is an %ETableGroup which groups by the nth + * grouping of the %ETableSortInfo. It creates %ETableGroups as + * children. + * + * Returns: The new %ETableGroupContainer. + */ +ETableGroup * +e_table_group_container_new (GnomeCanvasGroup *parent, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model, + ETableSortInfo *sort_info, + gint n) +{ + ETableGroupContainer *etgc; + + g_return_val_if_fail (parent != NULL, NULL); + + etgc = g_object_new (E_TYPE_TABLE_GROUP_CONTAINER, NULL); + + e_table_group_container_construct ( + parent, etgc, full_header, header, + model, sort_info, n); + return E_TABLE_GROUP (etgc); +} + +static gint +etgc_event (GnomeCanvasItem *item, + GdkEvent *event) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (item); + gboolean return_val = TRUE; + gboolean change_focus = FALSE; + gboolean use_col = FALSE; + gint start_col = 0; + gint old_col; + EFocus direction = E_FOCUS_START; + + switch (event->type) { + case GDK_KEY_PRESS: + if (event->key.keyval == GDK_KEY_Tab || + event->key.keyval == GDK_KEY_KP_Tab || + event->key.keyval == GDK_KEY_ISO_Left_Tab) { + change_focus = TRUE; + use_col = TRUE; + start_col = (event->key.state & GDK_SHIFT_MASK) ? -1 : 0; + direction = (event->key.state & GDK_SHIFT_MASK) ? E_FOCUS_END : E_FOCUS_START; + } else if (event->key.keyval == GDK_KEY_Left || + event->key.keyval == GDK_KEY_KP_Left) { + change_focus = TRUE; + use_col = TRUE; + start_col = -1; + direction = E_FOCUS_END; + } else if (event->key.keyval == GDK_KEY_Right || + event->key.keyval == GDK_KEY_KP_Right) { + change_focus = TRUE; + use_col = TRUE; + start_col = 0; + direction = E_FOCUS_START; + } else if (event->key.keyval == GDK_KEY_Down || + event->key.keyval == GDK_KEY_KP_Down) { + change_focus = TRUE; + use_col = FALSE; + direction = E_FOCUS_START; + } else if (event->key.keyval == GDK_KEY_Up || + event->key.keyval == GDK_KEY_KP_Up) { + change_focus = TRUE; + use_col = FALSE; + direction = E_FOCUS_END; + } else if (event->key.keyval == GDK_KEY_Return || + event->key.keyval == GDK_KEY_KP_Enter) { + change_focus = TRUE; + use_col = FALSE; + direction = E_FOCUS_START; + } + if (change_focus) { + GList *list; + for (list = etgc->children; list; list = list->next) { + ETableGroupContainerChildNode *child_node; + ETableGroup *child; + + child_node = (ETableGroupContainerChildNode *) list->data; + child = child_node->child; + + if (e_table_group_get_focus (child)) { + old_col = e_table_group_get_focus_column (child); + if (old_col == -1) + old_col = 0; + if (start_col == -1) + start_col = e_table_header_count (e_table_group_get_header (child)) - 1; + + if (direction == E_FOCUS_END) + list = list->prev; + else + list = list->next; + + if (list) { + child_node = (ETableGroupContainerChildNode *) list->data; + child = child_node->child; + if (use_col) + e_table_group_set_focus (child, direction, start_col); + else + e_table_group_set_focus (child, direction, old_col); + return 1; + } else { + return 0; + } + } + } + if (direction == E_FOCUS_END) + list = g_list_last (etgc->children); + else + list = etgc->children; + if (list) { + ETableGroupContainerChildNode *child_node; + ETableGroup *child; + + child_node = (ETableGroupContainerChildNode *) list->data; + child = child_node->child; + + if (start_col == -1) + start_col = e_table_header_count (e_table_group_get_header (child)) - 1; + + e_table_group_set_focus (child, direction, start_col); + return 1; + } + } + return_val = FALSE; + break; + default: + return_val = FALSE; + break; + } + if (return_val == FALSE) { + if (GNOME_CANVAS_ITEM_CLASS (etgc_parent_class)->event) + return GNOME_CANVAS_ITEM_CLASS (etgc_parent_class)->event (item, event); + } + return return_val; + +} + +/* Realize handler for the text item */ +static void +etgc_realize (GnomeCanvasItem *item) +{ + ETableGroupContainer *etgc; + + if (GNOME_CANVAS_ITEM_CLASS (etgc_parent_class)->realize) + (* GNOME_CANVAS_ITEM_CLASS (etgc_parent_class)->realize) (item); + + etgc = E_TABLE_GROUP_CONTAINER (item); + + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etgc)); +} + +/* Unrealize handler for the etgc item */ +static void +etgc_unrealize (GnomeCanvasItem *item) +{ + if (GNOME_CANVAS_ITEM_CLASS (etgc_parent_class)->unrealize) + (* GNOME_CANVAS_ITEM_CLASS (etgc_parent_class)->unrealize) (item); +} + +static void +compute_text (ETableGroupContainer *etgc, + ETableGroupContainerChildNode *child_node) +{ + gchar *text; + + if (etgc->ecol->text) { + /* Translators: This text is used as a special row when an ETable + * has turned on grouping on a column, which has set a title. + * The first %s is replaced with a column title. + * The second %s is replaced with an actual group value. + * Finally the %d is replaced with count of items in this group. + * Example: "Family name: Smith (13 items)" + */ + text = g_strdup_printf ( + ngettext ( + "%s: %s (%d item)", + "%s: %s (%d items)", + child_node->count), + etgc->ecol->text, child_node->string, + (gint) child_node->count); + } else { + /* Translators: This text is used as a special row when an ETable + * has turned on grouping on a column, which doesn't have set a title. + * The %s is replaced with an actual group value. + * The %d is replaced with count of items in this group. + * Example: "Smith (13 items)" + */ + text = g_strdup_printf ( + ngettext ( + "%s (%d item)", + "%s (%d items)", + child_node->count), + child_node->string, + (gint) child_node->count); + } + gnome_canvas_item_set ( + child_node->text, + "text", text, + NULL); + g_free (text); +} + +static void +child_cursor_change (ETableGroup *etg, + gint row, + ETableGroupContainer *etgc) +{ + e_table_group_cursor_change (E_TABLE_GROUP (etgc), row); +} + +static void +child_cursor_activated (ETableGroup *etg, + gint row, + ETableGroupContainer *etgc) +{ + e_table_group_cursor_activated (E_TABLE_GROUP (etgc), row); +} + +static void +child_double_click (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETableGroupContainer *etgc) +{ + e_table_group_double_click (E_TABLE_GROUP (etgc), row, col, event); +} + +static gboolean +child_right_click (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETableGroupContainer *etgc) +{ + return e_table_group_right_click (E_TABLE_GROUP (etgc), row, col, event); +} + +static gboolean +child_click (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETableGroupContainer *etgc) +{ + return e_table_group_click (E_TABLE_GROUP (etgc), row, col, event); +} + +static gboolean +child_key_press (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETableGroupContainer *etgc) +{ + return e_table_group_key_press (E_TABLE_GROUP (etgc), row, col, event); +} + +static gboolean +child_start_drag (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETableGroupContainer *etgc) +{ + return e_table_group_start_drag (E_TABLE_GROUP (etgc), row, col, event); +} + +static ETableGroupContainerChildNode * +create_child_node (ETableGroupContainer *etgc, + gpointer val) +{ + ETableGroup *child; + ETableGroupContainerChildNode *child_node; + ETableGroup *etg = E_TABLE_GROUP (etgc); + + child_node = g_new (ETableGroupContainerChildNode, 1); + child_node->rect = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (etgc), + gnome_canvas_rect_get_type (), + "fill_color", "grey70", + "outline_color", "grey50", + NULL); + child_node->text = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (etgc), + e_text_get_type (), + "fill_color", "black", + NULL); + child = e_table_group_new ( + GNOME_CANVAS_GROUP (etgc), etg->full_header, + etg->header, etg->model, etgc->sort_info, etgc->n + 1); + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (child), + "alternating_row_colors", etgc->alternating_row_colors, + "horizontal_draw_grid", etgc->horizontal_draw_grid, + "vertical_draw_grid", etgc->vertical_draw_grid, + "drawfocus", etgc->draw_focus, + "cursor_mode", etgc->cursor_mode, + "selection_model", etgc->selection_model, + "length_threshold", etgc->length_threshold, + "uniform_row_height", etgc->uniform_row_height, + "minimum_width", etgc->minimum_width - GROUP_INDENT, + NULL); + + g_signal_connect ( + child, "cursor_change", + G_CALLBACK (child_cursor_change), etgc); + g_signal_connect ( + child, "cursor_activated", + G_CALLBACK (child_cursor_activated), etgc); + g_signal_connect ( + child, "double_click", + G_CALLBACK (child_double_click), etgc); + g_signal_connect ( + child, "right_click", + G_CALLBACK (child_right_click), etgc); + g_signal_connect ( + child, "click", + G_CALLBACK (child_click), etgc); + g_signal_connect ( + child, "key_press", + G_CALLBACK (child_key_press), etgc); + g_signal_connect ( + child, "start_drag", + G_CALLBACK (child_start_drag), etgc); + child_node->child = child; + child_node->key = e_table_model_duplicate_value (etg->model, etgc->ecol->col_idx, val); + child_node->string = e_table_model_value_to_string (etg->model, etgc->ecol->col_idx, val); + child_node->count = 0; + + return child_node; +} + +static void +etgc_add (ETableGroup *etg, + gint row) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + gpointer val = e_table_model_value_at (etg->model, etgc->ecol->col_idx, row); + GCompareDataFunc comp = etgc->ecol->compare; + gpointer cmp_cache = e_table_sorting_utils_create_cmp_cache (); + GList *list = etgc->children; + ETableGroup *child; + ETableGroupContainerChildNode *child_node; + gint i = 0; + + for (; list; list = g_list_next (list), i++) { + gint comp_val; + + child_node = list->data; + comp_val = (*comp)(child_node->key, val, cmp_cache); + if (comp_val == 0) { + e_table_sorting_utils_free_cmp_cache (cmp_cache); + child = child_node->child; + child_node->count++; + e_table_group_add (child, row); + compute_text (etgc, child_node); + return; + } + if ((comp_val > 0 && etgc->ascending) || + (comp_val < 0 && (!etgc->ascending))) + break; + } + e_table_sorting_utils_free_cmp_cache (cmp_cache); + child_node = create_child_node (etgc, val); + child = child_node->child; + child_node->count = 1; + e_table_group_add (child, row); + + if (list) + etgc->children = g_list_insert (etgc->children, child_node, i); + else + etgc->children = g_list_append (etgc->children, child_node); + + compute_text (etgc, child_node); + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etgc)); +} + +static void +etgc_add_array (ETableGroup *etg, + const gint *array, + gint count) +{ + gint i; + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + gpointer lastval = NULL; + gint laststart = 0; + GCompareDataFunc comp = etgc->ecol->compare; + gpointer cmp_cache; + ETableGroupContainerChildNode *child_node; + ETableGroup *child; + + if (count <= 0) + return; + + e_table_group_container_list_free (etgc); + etgc->children = NULL; + cmp_cache = e_table_sorting_utils_create_cmp_cache (); + + lastval = e_table_model_value_at (etg->model, etgc->ecol->col_idx, array[0]); + + for (i = 1; i < count; i++) { + gpointer val = e_table_model_value_at (etg->model, etgc->ecol->col_idx, array[i]); + gint comp_val; + + comp_val = (*comp)(lastval, val, cmp_cache); + if (comp_val != 0) { + child_node = create_child_node (etgc, lastval); + child = child_node->child; + + e_table_group_add_array (child, array + laststart, i - laststart); + child_node->count = i - laststart; + + etgc->children = g_list_append (etgc->children, child_node); + compute_text (etgc, child_node); + laststart = i; + lastval = val; + } + } + + e_table_sorting_utils_free_cmp_cache (cmp_cache); + + child_node = create_child_node (etgc, lastval); + child = child_node->child; + + e_table_group_add_array (child, array + laststart, i - laststart); + child_node->count = i - laststart; + + etgc->children = g_list_append (etgc->children, child_node); + compute_text (etgc, child_node); + + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etgc)); +} + +static void +etgc_add_all (ETableGroup *etg) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + ESorter *sorter = etgc->selection_model->sorter; + gint *array; + gint count; + + e_sorter_get_sorted_to_model_array (sorter, &array, &count); + + etgc_add_array (etg, array, count); +} + +static gboolean +etgc_remove (ETableGroup *etg, + gint row) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + GList *list; + + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = list->data; + ETableGroup *child = child_node->child; + + if (e_table_group_remove (child, row)) { + child_node->count--; + if (child_node->count == 0) { + e_table_group_container_child_node_free (etgc, child_node); + etgc->children = g_list_remove (etgc->children, child_node); + g_free (child_node); + } else + compute_text (etgc, child_node); + + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etgc)); + + return TRUE; + } + } + return FALSE; +} + +static gint +etgc_row_count (ETableGroup *etg) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + GList *list; + gint count = 0; + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroup *group = ((ETableGroupContainerChildNode *) list->data)->child; + gint this_count = e_table_group_row_count (group); + count += this_count; + } + return count; +} + +static void +etgc_increment (ETableGroup *etg, + gint position, + gint amount) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + GList *list; + + for (list = etgc->children; list; list = g_list_next (list)) + e_table_group_increment ( + ((ETableGroupContainerChildNode *) list->data)->child, + position, amount); +} + +static void +etgc_decrement (ETableGroup *etg, + gint position, + gint amount) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + GList *list; + + for (list = etgc->children; list; list = g_list_next (list)) + e_table_group_decrement ( + ((ETableGroupContainerChildNode *) list->data)->child, + position, amount); +} + +static void +etgc_set_focus (ETableGroup *etg, + EFocus direction, + gint view_col) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + if (etgc->children) { + if (direction == E_FOCUS_END) + e_table_group_set_focus ( + ((ETableGroupContainerChildNode *) g_list_last (etgc->children)->data)->child, + direction, view_col); + else + e_table_group_set_focus ( + ((ETableGroupContainerChildNode *) etgc->children->data)->child, + direction, view_col); + } +} + +static gint +etgc_get_focus_column (ETableGroup *etg) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + if (etgc->children) { + GList *list; + for (list = etgc->children; list; list = list->next) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + ETableGroup *child = child_node->child; + if (e_table_group_get_focus (child)) { + return e_table_group_get_focus_column (child); + } + } + } + return 0; +} + +static void +etgc_compute_location (ETableGroup *etg, + gint *x, + gint *y, + gint *prow, + gint *pcol) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + gint row = -1, col = -1; + + *x -= GROUP_INDENT; + *y -= TITLE_HEIGHT; + + if (*x >= 0 && *y >= 0 && etgc->children) { + GList *list; + for (list = etgc->children; list; list = list->next) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + ETableGroup *child = child_node->child; + + e_table_group_compute_location (child, x, y, &row, &col); + if (row != -1 && col != -1) + break; + } + } + + if (prow) + *prow = row; + if (pcol) + *pcol = col; +} + +static void +etgc_get_mouse_over (ETableGroup *etg, + gint *row, + gint *col) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + + if (row) + *row = -1; + if (col) + *col = -1; + + if (etgc->children) { + gint row_plus = 0; + GList *list; + + for (list = etgc->children; list; list = list->next) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + ETableGroup *child = child_node->child; + + e_table_group_get_mouse_over (child, row, col); + + if ((!row || *row != -1) && (!col || *col != -1)) { + if (row) + *row += row_plus; + return; + } + + row_plus += e_table_group_row_count (child); + } + } +} + +static void +etgc_get_cell_geometry (ETableGroup *etg, + gint *row, + gint *col, + gint *x, + gint *y, + gint *width, + gint *height) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + + gint ypos; + + ypos = 0; + + if (etgc->children) { + GList *list; + for (list = etgc->children; list; list = list->next) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + ETableGroup *child = child_node->child; + gint thisy; + + e_table_group_get_cell_geometry (child, row, col, x, &thisy, width, height); + ypos += thisy; + if ((*row == -1) || (*col == -1)) { + ypos += TITLE_HEIGHT; + *x += GROUP_INDENT; + *y = ypos; + return; + } + } + } +} + +static void etgc_thaw (ETableGroup *etg) +{ + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (etg)); +} + +static void +etgc_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ETableGroup *etg = E_TABLE_GROUP (object); + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (object); + GList *list; + + switch (property_id) { + case PROP_FROZEN: + if (g_value_get_boolean (value)) + etg->frozen = TRUE; + else { + etg->frozen = FALSE; + etgc_thaw (etg); + } + break; + case PROP_MINIMUM_WIDTH: + case PROP_WIDTH: + etgc->minimum_width = g_value_get_double (value); + + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + g_object_set ( + child_node->child, + "minimum_width", etgc->minimum_width - GROUP_INDENT, + NULL); + } + break; + case PROP_LENGTH_THRESHOLD: + etgc->length_threshold = g_value_get_int (value); + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + g_object_set ( + child_node->child, + "length_threshold", etgc->length_threshold, + NULL); + } + break; + case PROP_UNIFORM_ROW_HEIGHT: + etgc->uniform_row_height = g_value_get_boolean (value); + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + g_object_set ( + child_node->child, + "uniform_row_height", etgc->uniform_row_height, + NULL); + } + break; + + case PROP_SELECTION_MODEL: + if (etgc->selection_model) + g_object_unref (etgc->selection_model); + etgc->selection_model = E_SELECTION_MODEL (g_value_get_object (value)); + if (etgc->selection_model) + g_object_ref (etgc->selection_model); + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + g_object_set ( + child_node->child, + "selection_model", etgc->selection_model, + NULL); + } + break; + + case PROP_TABLE_ALTERNATING_ROW_COLORS: + etgc->alternating_row_colors = g_value_get_boolean (value); + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + g_object_set ( + child_node->child, + "alternating_row_colors", etgc->alternating_row_colors, + NULL); + } + break; + + case PROP_TABLE_HORIZONTAL_DRAW_GRID: + etgc->horizontal_draw_grid = g_value_get_boolean (value); + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + g_object_set ( + child_node->child, + "horizontal_draw_grid", etgc->horizontal_draw_grid, + NULL); + } + break; + + case PROP_TABLE_VERTICAL_DRAW_GRID: + etgc->vertical_draw_grid = g_value_get_boolean (value); + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + g_object_set ( + child_node->child, + "vertical_draw_grid", etgc->vertical_draw_grid, + NULL); + } + break; + + case PROP_TABLE_DRAW_FOCUS: + etgc->draw_focus = g_value_get_boolean (value); + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + g_object_set ( + child_node->child, + "drawfocus", etgc->draw_focus, + NULL); + } + break; + + case PROP_CURSOR_MODE: + etgc->cursor_mode = g_value_get_int (value); + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + g_object_set ( + child_node->child, + "cursor_mode", etgc->cursor_mode, + NULL); + } + break; + default: + break; + } +} + +static void +etgc_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableGroup *etg = E_TABLE_GROUP (object); + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (object); + + switch (property_id) { + case PROP_FROZEN: + g_value_set_boolean (value, etg->frozen); + break; + case PROP_HEIGHT: + g_value_set_double (value, etgc->height); + break; + case PROP_WIDTH: + g_value_set_double (value, etgc->width); + break; + case PROP_MINIMUM_WIDTH: + g_value_set_double (value, etgc->minimum_width); + break; + case PROP_UNIFORM_ROW_HEIGHT: + g_value_set_boolean (value, etgc->uniform_row_height); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +etgc_class_init (ETableGroupContainerClass *class) +{ + GnomeCanvasItemClass *item_class = GNOME_CANVAS_ITEM_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + ETableGroupClass *e_group_class = E_TABLE_GROUP_CLASS (class); + + object_class->dispose = etgc_dispose; + object_class->set_property = etgc_set_property; + object_class->get_property = etgc_get_property; + + item_class->event = etgc_event; + item_class->realize = etgc_realize; + item_class->unrealize = etgc_unrealize; + + e_group_class->add = etgc_add; + e_group_class->add_array = etgc_add_array; + e_group_class->add_all = etgc_add_all; + e_group_class->remove = etgc_remove; + e_group_class->increment = etgc_increment; + e_group_class->decrement = etgc_decrement; + e_group_class->row_count = etgc_row_count; + e_group_class->set_focus = etgc_set_focus; + e_group_class->get_focus_column = etgc_get_focus_column; + e_group_class->get_printable = etgc_get_printable; + e_group_class->compute_location = etgc_compute_location; + e_group_class->get_mouse_over = etgc_get_mouse_over; + e_group_class->get_cell_geometry = etgc_get_cell_geometry; + + g_object_class_install_property ( + object_class, + PROP_TABLE_ALTERNATING_ROW_COLORS, + g_param_spec_boolean ( + "alternating_row_colors", + "Alternating Row Colors", + "Alternating Row Colors", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_HORIZONTAL_DRAW_GRID, + g_param_spec_boolean ( + "horizontal_draw_grid", + "Horizontal Draw Grid", + "Horizontal Draw Grid", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_VERTICAL_DRAW_GRID, + g_param_spec_boolean ( + "vertical_draw_grid", + "Vertical Draw Grid", + "Vertical Draw Grid", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_DRAW_FOCUS, + g_param_spec_boolean ( + "drawfocus", + "Draw focus", + "Draw focus", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_MODE, + g_param_spec_int ( + "cursor_mode", + "Cursor mode", + "Cursor mode", + E_CURSOR_LINE, + E_CURSOR_SPREADSHEET, + E_CURSOR_LINE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_SELECTION_MODEL, + g_param_spec_object ( + "selection_model", + "Selection model", + "Selection model", + E_TYPE_SELECTION_MODEL, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_LENGTH_THRESHOLD, + g_param_spec_int ( + "length_threshold", + "Length Threshold", + "Length Threshold", + -1, G_MAXINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_UNIFORM_ROW_HEIGHT, + g_param_spec_boolean ( + "uniform_row_height", + "Uniform row height", + "Uniform row height", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_FROZEN, + g_param_spec_boolean ( + "frozen", + "Frozen", + "Frozen", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_HEIGHT, + g_param_spec_double ( + "height", + "Height", + "Height", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WIDTH, + g_param_spec_double ( + "width", + "Width", + "Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MINIMUM_WIDTH, + g_param_spec_double ( + "minimum_width", + "Minimum width", + "Minimum Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); +} + +static void +etgc_reflow (GnomeCanvasItem *item, + gint flags) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (item); + gboolean frozen; + + g_object_get ( + etgc, + "frozen", &frozen, + NULL); + + if (frozen) + return; + + if (item->flags & GNOME_CANVAS_ITEM_REALIZED) { + gdouble running_height = 0; + gdouble running_width = 0; + gdouble old_height; + gdouble old_width; + + old_height = etgc->height; + old_width = etgc->width; + if (etgc->children == NULL) { + } else { + GList *list; + gdouble extra_height = 0; + gdouble item_height = 0; + gdouble item_width = 0; + + if (etgc->font_desc) { + PangoContext *context; + PangoFontMetrics *metrics; + + context = gtk_widget_get_pango_context (GTK_WIDGET (item->canvas)); + metrics = pango_context_get_metrics (context, etgc->font_desc, NULL); + extra_height += + PANGO_PIXELS (pango_font_metrics_get_ascent (metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (metrics)) + + BUTTON_PADDING * 2; + pango_font_metrics_unref (metrics); + } + + extra_height = MAX (extra_height, BUTTON_HEIGHT + BUTTON_PADDING * 2); + + running_height = extra_height; + + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + ETableGroup *child = child_node->child; + + g_object_get ( + child, + "width", &item_width, + NULL); + + if (item_width > running_width) + running_width = item_width; + } + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = (ETableGroupContainerChildNode *) list->data; + ETableGroup *child = child_node->child; + g_object_get ( + child, + "height", &item_height, + NULL); + + e_canvas_item_move_absolute ( + GNOME_CANVAS_ITEM (child_node->text), + GROUP_INDENT, + running_height - GROUP_INDENT - BUTTON_PADDING); + + e_canvas_item_move_absolute ( + GNOME_CANVAS_ITEM (child), + GROUP_INDENT, + running_height); + + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (child_node->rect), + "x1", (gdouble) 0, + "x2", (gdouble) running_width + GROUP_INDENT, + "y1", (gdouble) running_height - extra_height, + "y2", (gdouble) running_height + item_height, + NULL); + + running_height += item_height + extra_height; + } + running_height -= extra_height; + } + if (running_height != old_height || running_width != old_width) { + etgc->height = running_height; + etgc->width = running_width; + e_canvas_item_request_parent_reflow (item); + } + } +} + +static void +etgc_init (ETableGroupContainer *container) +{ + container->children = NULL; + + e_canvas_item_set_reflow_callback (GNOME_CANVAS_ITEM (container), etgc_reflow); + + container->alternating_row_colors = 1; + container->horizontal_draw_grid = 1; + container->vertical_draw_grid = 1; + container->draw_focus = 1; + container->cursor_mode = E_CURSOR_SIMPLE; + container->length_threshold = -1; + container->selection_model = NULL; + container->uniform_row_height = FALSE; +} + +void +e_table_group_apply_to_leafs (ETableGroup *etg, + ETableGroupLeafFn fn, + gpointer closure) +{ + if (E_IS_TABLE_GROUP_CONTAINER (etg)) { + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + GList *list; + + /* Protect from unrefs in the callback functions */ + g_object_ref (etg); + + for (list = etgc->children; list; list = list->next) { + ETableGroupContainerChildNode *child_node = list->data; + + e_table_group_apply_to_leafs (child_node->child, fn, closure); + } + + g_object_unref (etg); + } else if (E_IS_TABLE_GROUP_LEAF (etg)) { + (*fn) (E_TABLE_GROUP_LEAF (etg)->item, closure); + } else { + g_error ( + "Unknown ETableGroup found: %s", + g_type_name (G_TYPE_FROM_INSTANCE (etg))); + } +} + +typedef struct { + ETableGroupContainer *etgc; + GList *child; + EPrintable *child_printable; +} ETGCPrintContext; + +#define CHECK(x) if((x) == -1) return -1; + +#if 0 +static gint +gp_draw_rect (GtkPrintContext *context, + gdouble x, + gdouble y, + gdouble width, + gdouble height) +{ + cairo_t *cr; + cr = gtk_print_context_get_cairo_context (context); + cairo_move_to (cr, x, y); + cairo_rectangle (cr, x, y, x + width, y + height); + cairo_fill (cr); +} +#endif + +#define TEXT_HEIGHT (12) +#define TEXT_AREA_HEIGHT (TEXT_HEIGHT + 4) + +static void +e_table_group_container_print_page (EPrintable *ep, + GtkPrintContext *context, + gdouble width, + gdouble height, + gboolean quantize, + ETGCPrintContext *groupcontext) +{ + cairo_t *cr = NULL; + GtkPageSetup *setup; + gdouble yd; + gdouble page_height, page_margin; + gdouble child_height, child_margin = 0; + ETableGroupContainerChildNode *child_node; + GList *child; + EPrintable *child_printable; + gchar *string; + PangoLayout *layout; + PangoFontDescription *desc; + + child_printable = groupcontext->child_printable; + child = groupcontext->child; + setup = gtk_print_context_get_page_setup (context); + page_height = gtk_page_setup_get_page_height (setup, GTK_UNIT_POINTS); + page_margin = gtk_page_setup_get_bottom_margin (setup, GTK_UNIT_POINTS) + gtk_page_setup_get_top_margin (setup, GTK_UNIT_POINTS); + yd = page_height - page_margin; + + if (child_printable) { + if (child) + child_node = child->data; + else + child_node = NULL; + g_object_ref (child_printable); + } else { + if (!child) { + return; + } else { + child_node = child->data; + child_printable = e_table_group_get_printable (child_node->child); + if (child_printable) + g_object_ref (child_printable); + e_printable_reset (child_printable); + } + } + + layout = gtk_print_context_create_pango_layout (context); + + desc = pango_font_description_new (); + pango_font_description_set_family_static (desc, "Helvetica"); + pango_font_description_set_size (desc, TEXT_HEIGHT); + pango_layout_set_font_description (layout, desc); + pango_font_description_free (desc); + + while (1) { + child_height = e_printable_height (child_printable, context, width,yd, quantize); + if (child_height < 0) + child_height = -child_height; + if (cr && yd < 2 * TEXT_AREA_HEIGHT + 20 + child_height) { + cairo_show_page (cr); + cairo_translate (cr, -2 * TEXT_AREA_HEIGHT, -TEXT_AREA_HEIGHT); + break; + } + + cr = gtk_print_context_get_cairo_context (context); + cairo_save (cr); + cairo_rectangle (cr, 0.0, 0.0, width, TEXT_AREA_HEIGHT); + cairo_rectangle (cr, 0.0, 0.0, 2 * TEXT_AREA_HEIGHT, child_height + 2 * TEXT_AREA_HEIGHT); + cairo_set_source_rgb (cr, .7, .7, .7); + cairo_fill (cr); + cairo_restore (cr); + child_margin = TEXT_AREA_HEIGHT; + + cairo_save (cr); + cairo_rectangle (cr, 2 * TEXT_AREA_HEIGHT, TEXT_AREA_HEIGHT, width - 2 * TEXT_AREA_HEIGHT, TEXT_AREA_HEIGHT); + cairo_clip (cr); + cairo_restore (cr); + + if (child_node) { + cairo_move_to (cr, 0, 0); + if (groupcontext->etgc->ecol->text) + string = g_strdup_printf ( + "%s : %s (%d item%s)", + groupcontext->etgc->ecol->text, + child_node->string, + (gint) child_node->count, + child_node->count == 1 ? "" : "s"); + else + string = g_strdup_printf ( + "%s (%d item%s)", + child_node->string, + (gint) child_node->count, + child_node->count == 1 ? "" : "s"); + pango_layout_set_text (layout, string, -1); + pango_cairo_show_layout (cr, layout); + g_free (string); + } + + cairo_translate (cr, 2 * TEXT_AREA_HEIGHT, TEXT_AREA_HEIGHT); + cairo_move_to (cr, 0, 0); + cairo_save (cr); + cairo_rectangle (cr, 0, child_margin, width - 2 * TEXT_AREA_HEIGHT, child_height + child_margin + 20); + cairo_clip (cr); + + e_printable_print_page (child_printable, context, width - 2 * TEXT_AREA_HEIGHT, child_margin, quantize); + yd -= child_height + TEXT_AREA_HEIGHT; + + if (e_printable_data_left (child_printable)) { + cairo_restore (cr); + cairo_translate (cr, -2 * TEXT_AREA_HEIGHT, -TEXT_AREA_HEIGHT); + break; + } + + child = child->next; + if (!child) { + child_printable = NULL; + break; + } + + child_node = child->data; + if (child_printable) + g_object_unref (child_printable); + + child_printable = e_table_group_get_printable (child_node->child); + cairo_restore (cr); + cairo_translate (cr, -2 * TEXT_AREA_HEIGHT, child_height + child_margin + 20); + + if (child_printable) + g_object_ref (child_printable); + e_printable_reset (child_printable); + } + if (groupcontext->child_printable) + g_object_unref (groupcontext->child_printable); + groupcontext->child_printable = child_printable; + groupcontext->child = child; + + g_object_unref (layout); +} + +static gboolean +e_table_group_container_data_left (EPrintable *ep, + ETGCPrintContext *groupcontext) +{ + g_signal_stop_emission_by_name (ep, "data_left"); + return groupcontext->child != NULL; +} + +static void +e_table_group_container_reset (EPrintable *ep, + ETGCPrintContext *groupcontext) +{ + groupcontext->child = groupcontext->etgc->children; + if (groupcontext->child_printable) + g_object_unref (groupcontext->child_printable); + groupcontext->child_printable = NULL; +} + +static gdouble +e_table_group_container_height (EPrintable *ep, + GtkPrintContext *context, + gdouble width, + gdouble max_height, + gboolean quantize, + ETGCPrintContext *groupcontext) +{ + gdouble height = 0; + gdouble child_height; + gdouble yd = max_height; + ETableGroupContainerChildNode *child_node; + GList *child; + EPrintable *child_printable; + + child_printable = groupcontext->child_printable; + child = groupcontext->child; + + if (child_printable) + g_object_ref (child_printable); + else { + if (!child) { + g_signal_stop_emission_by_name (ep, "height"); + return 0; + } else { + child_node = child->data; + child_printable = e_table_group_get_printable (child_node->child); + if (child_printable) + g_object_ref (child_printable); + e_printable_reset (child_printable); + } + } + + if (yd != -1 && yd < TEXT_AREA_HEIGHT) + return 0; + + while (1) { + child_height = e_printable_height (child_printable, context, width - 36, yd - (yd == -1 ? 0 : TEXT_AREA_HEIGHT), quantize); + + height -= child_height + TEXT_AREA_HEIGHT; + + if (yd != -1) { + if (!e_printable_will_fit (child_printable, context, width - 36, yd - (yd == -1 ? 0 : TEXT_AREA_HEIGHT), quantize)) { + break; + } + + yd += child_height + TEXT_AREA_HEIGHT; + } + + child = child->next; + if (!child) { + break; + } + + child_node = child->data; + if (child_printable) + g_object_unref (child_printable); + child_printable = e_table_group_get_printable (child_node->child); + if (child_printable) + g_object_ref (child_printable); + e_printable_reset (child_printable); + } + if (child_printable) + g_object_unref (child_printable); + g_signal_stop_emission_by_name (ep, "height"); + return height; +} + +static gboolean +e_table_group_container_will_fit (EPrintable *ep, + GtkPrintContext *context, + gdouble width, + gdouble max_height, + gboolean quantize, + ETGCPrintContext *groupcontext) +{ + gboolean will_fit = TRUE; + gdouble child_height; + gdouble yd = max_height; + ETableGroupContainerChildNode *child_node; + GList *child; + EPrintable *child_printable; + + child_printable = groupcontext->child_printable; + child = groupcontext->child; + + if (child_printable) + g_object_ref (child_printable); + else { + if (!child) { + g_signal_stop_emission_by_name (ep, "will_fit"); + return will_fit; + } else { + child_node = child->data; + child_printable = e_table_group_get_printable (child_node->child); + if (child_printable) + g_object_ref (child_printable); + e_printable_reset (child_printable); + } + } + + if (yd != -1 && yd < TEXT_AREA_HEIGHT) + will_fit = FALSE; + else { + while (1) { + child_height = e_printable_height (child_printable, context, width - 36, yd - (yd == -1 ? 0 : TEXT_AREA_HEIGHT), quantize); + + if (yd != -1) { + if (!e_printable_will_fit (child_printable, context, width - 36, yd - (yd == -1 ? 0 : TEXT_AREA_HEIGHT), quantize)) { + will_fit = FALSE; + break; + } + + yd += child_height + TEXT_AREA_HEIGHT; + } + + child = child->next; + if (!child) { + break; + } + + child_node = child->data; + if (child_printable) + g_object_unref (child_printable); + child_printable = e_table_group_get_printable (child_node->child); + if (child_printable) + g_object_ref (child_printable); + e_printable_reset (child_printable); + } + } + + if (child_printable) + g_object_unref (child_printable); + + g_signal_stop_emission_by_name (ep, "will_fit"); + return will_fit; +} + +static void +e_table_group_container_printable_destroy (gpointer data, + GObject *where_object_was) + +{ + ETGCPrintContext *groupcontext = data; + + g_object_unref (groupcontext->etgc); + if (groupcontext->child_printable) + g_object_ref (groupcontext->child_printable); + g_free (groupcontext); +} + +static EPrintable * +etgc_get_printable (ETableGroup *etg) +{ + ETableGroupContainer *etgc = E_TABLE_GROUP_CONTAINER (etg); + EPrintable *printable = e_printable_new (); + ETGCPrintContext *groupcontext; + + groupcontext = g_new (ETGCPrintContext, 1); + groupcontext->etgc = etgc; + g_object_ref (etgc); + groupcontext->child = etgc->children; + groupcontext->child_printable = NULL; + + g_signal_connect ( + printable, "print_page", + G_CALLBACK (e_table_group_container_print_page), + groupcontext); + g_signal_connect ( + printable, "data_left", + G_CALLBACK (e_table_group_container_data_left), + groupcontext); + g_signal_connect ( + printable, "reset", + G_CALLBACK (e_table_group_container_reset), + groupcontext); + g_signal_connect ( + printable, "height", + G_CALLBACK (e_table_group_container_height), + groupcontext); + g_signal_connect ( + printable, "will_fit", + G_CALLBACK (e_table_group_container_will_fit), + groupcontext); + g_object_weak_ref ( + G_OBJECT (printable), + e_table_group_container_printable_destroy, + groupcontext); + + return printable; +} diff --git a/e-util/e-table-group-container.h b/e-util/e-table-group-container.h new file mode 100644 index 0000000000..3f6fb03b7a --- /dev/null +++ b/e-util/e-table-group-container.h @@ -0,0 +1,138 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_GROUP_CONTAINER_H_ +#define _E_TABLE_GROUP_CONTAINER_H_ + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-table-group.h> +#include <e-util/e-table-header.h> +#include <e-util/e-table-item.h> +#include <e-util/e-table-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_GROUP_CONTAINER \ + (e_table_group_container_get_type ()) +#define E_TABLE_GROUP_CONTAINER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_GROUP_CONTAINER, ETableGroupContainer)) +#define E_TABLE_GROUP_CONTAINER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_GROUP_CONTAINER, ETableGroupContainerClass)) +#define E_IS_TABLE_GROUP_CONTAINER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_GROUP_CONTAINER)) +#define E_IS_TABLE_GROUP_CONTAINER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_GROUP_CONTAINER)) +#define E_TABLE_GROUP_CONTAINER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_GROUP_CONTAINER, ETableGroupContainerClass)) + +G_BEGIN_DECLS + +typedef struct _ETableGroupContainer ETableGroupContainer; +typedef struct _ETableGroupContainerClass ETableGroupContainerClass; + +typedef struct _ETableGroupContainerChildNode ETableGroupContainerChildNode; + +struct _ETableGroupContainer { + ETableGroup group; + + /* + * The ETableCol used to group this set + */ + ETableCol *ecol; + gint ascending; + + /* + * List of ETableGroups we stack + */ + GList *children; + + /* + * The canvas rectangle that contains the children + */ + GnomeCanvasItem *rect; + + PangoFontDescription *font_desc; + + gdouble width, height, minimum_width; + + ETableSortInfo *sort_info; + gint n; + gint length_threshold; + + ESelectionModel *selection_model; + + guint alternating_row_colors : 1; + guint horizontal_draw_grid : 1; + guint vertical_draw_grid : 1; + guint draw_focus : 1; + guint uniform_row_height : 1; + ECursorMode cursor_mode; + + /* + * State: the ETableGroup is open or closed + */ + guint open : 1; +}; + +struct _ETableGroupContainerClass { + ETableGroupClass parent_class; +}; + +struct _ETableGroupContainerChildNode { + ETableGroup *child; + gpointer key; + gchar *string; + GnomeCanvasItem *text; + GnomeCanvasItem *rect; + gint count; +}; + +GType e_table_group_container_get_type + (void) G_GNUC_CONST; +ETableGroup * e_table_group_container_new (GnomeCanvasGroup *parent, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model, + ETableSortInfo *sort_info, + gint n); +void e_table_group_container_construct + (GnomeCanvasGroup *parent, + ETableGroupContainer *etgc, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model, + ETableSortInfo *sort_info, + gint n); + +G_END_DECLS + +#endif /* _E_TABLE_GROUP_CONTAINER_H_ */ diff --git a/e-util/e-table-group-leaf.c b/e-util/e-table-group-leaf.c new file mode 100644 index 0000000000..8d1a91da69 --- /dev/null +++ b/e-util/e-table-group-leaf.c @@ -0,0 +1,816 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-group-leaf.h" + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-canvas.h" +#include "e-table-item.h" +#include "e-table-sorted.h" +#include "e-table-sorted-variable.h" + +/* workaround for avoiding APi breakage */ +#define etgl_get_type e_table_group_leaf_get_type +G_DEFINE_TYPE (ETableGroupLeaf, etgl, E_TYPE_TABLE_GROUP) + +enum { + PROP_0, + PROP_HEIGHT, + PROP_WIDTH, + PROP_MINIMUM_WIDTH, + PROP_FROZEN, + PROP_TABLE_ALTERNATING_ROW_COLORS, + PROP_TABLE_HORIZONTAL_DRAW_GRID, + PROP_TABLE_VERTICAL_DRAW_GRID, + PROP_TABLE_DRAW_FOCUS, + PROP_CURSOR_MODE, + PROP_LENGTH_THRESHOLD, + PROP_SELECTION_MODEL, + PROP_UNIFORM_ROW_HEIGHT +}; + +static void +etgl_dispose (GObject *object) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (object); + + if (etgl->ets) { + g_object_unref (etgl->ets); + etgl->ets = NULL; + } + + if (etgl->item) { + if (etgl->etgl_cursor_change_id != 0) + g_signal_handler_disconnect ( + etgl->item, + etgl->etgl_cursor_change_id); + if (etgl->etgl_cursor_activated_id != 0) + g_signal_handler_disconnect ( + etgl->item, + etgl->etgl_cursor_activated_id); + if (etgl->etgl_double_click_id != 0) + g_signal_handler_disconnect ( + etgl->item, + etgl->etgl_double_click_id); + if (etgl->etgl_right_click_id != 0) + g_signal_handler_disconnect ( + etgl->item, + etgl->etgl_right_click_id); + if (etgl->etgl_click_id != 0) + g_signal_handler_disconnect ( + etgl->item, + etgl->etgl_click_id); + if (etgl->etgl_key_press_id != 0) + g_signal_handler_disconnect ( + etgl->item, + etgl->etgl_key_press_id); + if (etgl->etgl_start_drag_id != 0) + g_signal_handler_disconnect ( + etgl->item, + etgl->etgl_start_drag_id); + + etgl->etgl_cursor_change_id = 0; + etgl->etgl_cursor_activated_id = 0; + etgl->etgl_double_click_id = 0; + etgl->etgl_right_click_id = 0; + etgl->etgl_click_id = 0; + etgl->etgl_key_press_id = 0; + etgl->etgl_start_drag_id = 0; + + g_object_run_dispose (G_OBJECT (etgl->item)); + etgl->item = NULL; + } + + if (etgl->selection_model) { + g_object_unref (etgl->selection_model); + etgl->selection_model = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (etgl_parent_class)->dispose (object); +} + +static void +e_table_group_leaf_construct (GnomeCanvasGroup *parent, + ETableGroupLeaf *etgl, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model, + ETableSortInfo *sort_info) +{ + etgl->is_grouped = + (e_table_sort_info_grouping_get_count (sort_info) > 0); + + if (etgl->is_grouped) + etgl->ets = E_TABLE_SUBSET ( + e_table_sorted_variable_new ( + model, full_header, sort_info)); + else + etgl->ets = E_TABLE_SUBSET ( + e_table_sorted_new ( + model, full_header, sort_info)); + + e_table_group_construct ( + parent, E_TABLE_GROUP (etgl), full_header, header, model); +} + +/** + * e_table_group_leaf_new + * @parent: The %GnomeCanvasGroup to create a child of. + * @full_header: The full header of the %ETable. + * @header: The current header of the %ETable. + * @model: The %ETableModel of the %ETable. + * @sort_info: The %ETableSortInfo of the %ETable. + * + * %ETableGroupLeaf is an %ETableGroup which simply contains an + * %ETableItem. + * + * Returns: The new %ETableGroupLeaf. + */ +ETableGroup * +e_table_group_leaf_new (GnomeCanvasGroup *parent, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model, + ETableSortInfo *sort_info) +{ + ETableGroupLeaf *etgl; + + g_return_val_if_fail (parent != NULL, NULL); + + etgl = g_object_new (E_TYPE_TABLE_GROUP_LEAF, NULL); + + e_table_group_leaf_construct ( + parent, etgl, full_header, + header, model, sort_info); + + return E_TABLE_GROUP (etgl); +} + +static void +etgl_cursor_change (GObject *object, + gint row, + ETableGroupLeaf *etgl) +{ + if (row < E_TABLE_SUBSET (etgl->ets)->n_map) + e_table_group_cursor_change ( + E_TABLE_GROUP (etgl), + E_TABLE_SUBSET (etgl->ets)->map_table[row]); +} + +static void +etgl_cursor_activated (GObject *object, + gint view_row, + ETableGroupLeaf *etgl) +{ + if (view_row < E_TABLE_SUBSET (etgl->ets)->n_map) + e_table_group_cursor_activated ( + E_TABLE_GROUP (etgl), + E_TABLE_SUBSET (etgl->ets)->map_table[view_row]); +} + +static void +etgl_double_click (GObject *object, + gint model_row, + gint model_col, + GdkEvent *event, + ETableGroupLeaf *etgl) +{ + e_table_group_double_click ( + E_TABLE_GROUP (etgl), model_row, model_col, event); +} + +static gboolean +etgl_key_press (GObject *object, + gint row, + gint col, + GdkEvent *event, + ETableGroupLeaf *etgl) +{ + if (row < E_TABLE_SUBSET (etgl->ets)->n_map && row >= 0) + return e_table_group_key_press ( + E_TABLE_GROUP (etgl), + E_TABLE_SUBSET (etgl->ets)->map_table[row], + col, event); + else + return FALSE; +} + +static gboolean +etgl_start_drag (GObject *object, + gint model_row, + gint model_col, + GdkEvent *event, + ETableGroupLeaf *etgl) +{ + return e_table_group_start_drag ( + E_TABLE_GROUP (etgl), model_row, model_col, event); +} + +static gboolean +etgl_right_click (GObject *object, + gint view_row, + gint model_col, + GdkEvent *event, + ETableGroupLeaf *etgl) +{ + if (view_row < E_TABLE_SUBSET (etgl->ets)->n_map) + return e_table_group_right_click ( + E_TABLE_GROUP (etgl), + E_TABLE_SUBSET (etgl->ets)->map_table[view_row], + model_col, event); + else + return FALSE; +} + +static gboolean +etgl_click (GObject *object, + gint row, + gint col, + GdkEvent *event, + ETableGroupLeaf *etgl) +{ + if (row < E_TABLE_SUBSET (etgl->ets)->n_map) + return e_table_group_click ( + E_TABLE_GROUP (etgl), + E_TABLE_SUBSET (etgl->ets)->map_table[row], + col, event); + else + return FALSE; +} + +static void +etgl_reflow (GnomeCanvasItem *item, + gint flags) +{ + ETableGroupLeaf *leaf = E_TABLE_GROUP_LEAF (item); + + g_object_get (leaf->item, "height", &leaf->height, NULL); + g_object_get (leaf->item, "width", &leaf->width, NULL); + + e_canvas_item_request_parent_reflow (item); +} + +static void +etgl_realize (GnomeCanvasItem *item) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (item); + + if (GNOME_CANVAS_ITEM_CLASS (etgl_parent_class)->realize) + GNOME_CANVAS_ITEM_CLASS (etgl_parent_class)->realize (item); + + etgl->item = E_TABLE_ITEM (gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (etgl), + e_table_item_get_type (), + "ETableHeader", E_TABLE_GROUP (etgl)->header, + "ETableModel", etgl->ets, + "alternating_row_colors", etgl->alternating_row_colors, + "horizontal_draw_grid", etgl->horizontal_draw_grid, + "vertical_draw_grid", etgl->vertical_draw_grid, + "drawfocus", etgl->draw_focus, + "cursor_mode", etgl->cursor_mode, + "minimum_width", etgl->minimum_width, + "length_threshold", etgl->length_threshold, + "selection_model", etgl->selection_model, + "uniform_row_height", etgl->uniform_row_height, + NULL)); + + etgl->etgl_cursor_change_id = g_signal_connect ( + etgl->item, "cursor_change", + G_CALLBACK (etgl_cursor_change), etgl); + + etgl->etgl_cursor_activated_id = g_signal_connect ( + etgl->item, "cursor_activated", + G_CALLBACK (etgl_cursor_activated), etgl); + + etgl->etgl_double_click_id = g_signal_connect ( + etgl->item, "double_click", + G_CALLBACK (etgl_double_click), etgl); + + etgl->etgl_right_click_id = g_signal_connect ( + etgl->item, "right_click", + G_CALLBACK (etgl_right_click), etgl); + + etgl->etgl_click_id = g_signal_connect ( + etgl->item, "click", + G_CALLBACK (etgl_click), etgl); + + etgl->etgl_key_press_id = g_signal_connect ( + etgl->item, "key_press", + G_CALLBACK (etgl_key_press), etgl); + + etgl->etgl_start_drag_id = g_signal_connect ( + etgl->item, "start_drag", + G_CALLBACK (etgl_start_drag), etgl); + + e_canvas_item_request_reflow (item); +} + +static void +etgl_add (ETableGroup *etg, + gint row) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + if (E_IS_TABLE_SUBSET_VARIABLE (etgl->ets)) { + e_table_subset_variable_add ( + E_TABLE_SUBSET_VARIABLE (etgl->ets), row); + } +} + +static void +etgl_add_array (ETableGroup *etg, + const gint *array, + gint count) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + if (E_IS_TABLE_SUBSET_VARIABLE (etgl->ets)) { + e_table_subset_variable_add_array ( + E_TABLE_SUBSET_VARIABLE (etgl->ets), array, count); + } +} + +static void +etgl_add_all (ETableGroup *etg) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + if (E_IS_TABLE_SUBSET_VARIABLE (etgl->ets)) { + e_table_subset_variable_add_all ( + E_TABLE_SUBSET_VARIABLE (etgl->ets)); + } +} + +static gboolean +etgl_remove (ETableGroup *etg, + gint row) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + if (E_IS_TABLE_SUBSET_VARIABLE (etgl->ets)) { + return e_table_subset_variable_remove ( + E_TABLE_SUBSET_VARIABLE (etgl->ets), row); + } + return FALSE; +} + +static void +etgl_increment (ETableGroup *etg, + gint position, + gint amount) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + if (E_IS_TABLE_SUBSET_VARIABLE (etgl->ets)) { + e_table_subset_variable_increment ( + E_TABLE_SUBSET_VARIABLE (etgl->ets), + position, amount); + } +} + +static void +etgl_decrement (ETableGroup *etg, + gint position, + gint amount) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + if (E_IS_TABLE_SUBSET_VARIABLE (etgl->ets)) { + e_table_subset_variable_decrement ( + E_TABLE_SUBSET_VARIABLE (etgl->ets), + position, amount); + } +} + +static gint +etgl_row_count (ETableGroup *etg) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + return e_table_model_row_count (E_TABLE_MODEL (etgl->ets)); +} + +static void +etgl_set_focus (ETableGroup *etg, + EFocus direction, + gint view_col) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + if (direction == E_FOCUS_END) { + e_table_item_set_cursor ( + etgl->item, view_col, + e_table_model_row_count (E_TABLE_MODEL (etgl->ets)) - 1); + } else { + e_table_item_set_cursor (etgl->item, view_col, 0); + } +} + +static gint +etgl_get_focus_column (ETableGroup *etg) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + return e_table_item_get_focused_column (etgl->item); +} + +static EPrintable * +etgl_get_printable (ETableGroup *etg) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + return e_table_item_get_printable (etgl->item); +} + +static void +etgl_compute_location (ETableGroup *etg, + gint *x, + gint *y, + gint *row, + gint *col) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + e_table_item_compute_location (etgl->item, x, y, row, col); +} + +static void +etgl_get_mouse_over (ETableGroup *etg, + gint *row, + gint *col) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + if (etgl->item && etgl->item->motion_row > -1 && etgl->item->motion_col > -1) { + if (row) + *row = etgl->item->motion_row; + if (col) + *col = etgl->item->motion_col; + } +} + +static void +etgl_get_cell_geometry (ETableGroup *etg, + gint *row, + gint *col, + gint *x, + gint *y, + gint *width, + gint *height) +{ + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (etg); + + e_table_item_get_cell_geometry (etgl->item, row, col, x, y, width, height); +} + +static void +etgl_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ETableGroup *etg = E_TABLE_GROUP (object); + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (object); + + switch (property_id) { + case PROP_FROZEN: + etg->frozen = g_value_get_boolean (value); + break; + case PROP_MINIMUM_WIDTH: + case PROP_WIDTH: + etgl->minimum_width = g_value_get_double (value); + if (etgl->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etgl->item), + "minimum_width", etgl->minimum_width, + NULL); + } + break; + case PROP_LENGTH_THRESHOLD: + etgl->length_threshold = g_value_get_int (value); + if (etgl->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etgl->item), + "length_threshold", etgl->length_threshold, + NULL); + } + break; + case PROP_SELECTION_MODEL: + if (etgl->selection_model) + g_object_unref (etgl->selection_model); + etgl->selection_model = E_SELECTION_MODEL (g_value_get_object (value)); + if (etgl->selection_model) { + g_object_ref (etgl->selection_model); + } + if (etgl->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etgl->item), + "selection_model", etgl->selection_model, + NULL); + } + break; + + case PROP_UNIFORM_ROW_HEIGHT: + etgl->uniform_row_height = g_value_get_boolean (value); + if (etgl->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etgl->item), + "uniform_row_height", etgl->uniform_row_height, + NULL); + } + break; + + case PROP_TABLE_ALTERNATING_ROW_COLORS: + etgl->alternating_row_colors = g_value_get_boolean (value); + if (etgl->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etgl->item), + "alternating_row_colors", etgl->alternating_row_colors, + NULL); + } + break; + + case PROP_TABLE_HORIZONTAL_DRAW_GRID: + etgl->horizontal_draw_grid = g_value_get_boolean (value); + if (etgl->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etgl->item), + "horizontal_draw_grid", etgl->horizontal_draw_grid, + NULL); + } + break; + + case PROP_TABLE_VERTICAL_DRAW_GRID: + etgl->vertical_draw_grid = g_value_get_boolean (value); + if (etgl->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etgl->item), + "vertical_draw_grid", etgl->vertical_draw_grid, + NULL); + } + break; + + case PROP_TABLE_DRAW_FOCUS: + etgl->draw_focus = g_value_get_boolean (value); + if (etgl->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etgl->item), + "drawfocus", etgl->draw_focus, + NULL); + } + break; + + case PROP_CURSOR_MODE: + etgl->cursor_mode = g_value_get_int (value); + if (etgl->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etgl->item), + "cursor_mode", etgl->cursor_mode, + NULL); + } + break; + default: + break; + } +} + +static void +etgl_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableGroup *etg = E_TABLE_GROUP (object); + ETableGroupLeaf *etgl = E_TABLE_GROUP_LEAF (object); + + switch (property_id) { + case PROP_FROZEN: + g_value_set_boolean (value, etg->frozen); + break; + case PROP_HEIGHT: + g_value_set_double (value, etgl->height); + break; + case PROP_WIDTH: + g_value_set_double (value, etgl->width); + break; + case PROP_MINIMUM_WIDTH: + g_value_set_double (value, etgl->minimum_width); + break; + case PROP_UNIFORM_ROW_HEIGHT: + g_value_set_boolean (value, etgl->uniform_row_height); + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +etgl_class_init (ETableGroupLeafClass *class) +{ + GnomeCanvasItemClass *item_class = GNOME_CANVAS_ITEM_CLASS (class); + ETableGroupClass *e_group_class = E_TABLE_GROUP_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = etgl_dispose; + object_class->set_property = etgl_set_property; + object_class->get_property = etgl_get_property; + + item_class->realize = etgl_realize; + + e_group_class->add = etgl_add; + e_group_class->add_array = etgl_add_array; + e_group_class->add_all = etgl_add_all; + e_group_class->remove = etgl_remove; + e_group_class->increment = etgl_increment; + e_group_class->decrement = etgl_decrement; + e_group_class->row_count = etgl_row_count; + e_group_class->set_focus = etgl_set_focus; + e_group_class->get_focus_column = etgl_get_focus_column; + e_group_class->get_printable = etgl_get_printable; + e_group_class->compute_location = etgl_compute_location; + e_group_class->get_mouse_over = etgl_get_mouse_over; + e_group_class->get_cell_geometry = etgl_get_cell_geometry; + + g_object_class_install_property ( + object_class, + PROP_TABLE_ALTERNATING_ROW_COLORS, + g_param_spec_boolean ( + "alternating_row_colors", + "Alternating Row Colors", + "Alternating Row Colors", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_HORIZONTAL_DRAW_GRID, + g_param_spec_boolean ( + "horizontal_draw_grid", + "Horizontal Draw Grid", + "Horizontal Draw Grid", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_VERTICAL_DRAW_GRID, + g_param_spec_boolean ( + "vertical_draw_grid", + "Vertical Draw Grid", + "Vertical Draw Grid", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_DRAW_FOCUS, + g_param_spec_boolean ( + "drawfocus", + "Draw focus", + "Draw focus", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_MODE, + g_param_spec_int ( + "cursor_mode", + "Cursor mode", + "Cursor mode", + E_CURSOR_LINE, + E_CURSOR_SPREADSHEET, + E_CURSOR_LINE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_LENGTH_THRESHOLD, + g_param_spec_int ( + "length_threshold", + "Length Threshold", + "Length Threshold", + -1, G_MAXINT, 0, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_SELECTION_MODEL, + g_param_spec_object ( + "selection_model", + "Selection model", + "Selection model", + E_TYPE_SELECTION_MODEL, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_HEIGHT, + g_param_spec_double ( + "height", + "Height", + "Height", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_WIDTH, + g_param_spec_double ( + "width", + "Width", + "Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MINIMUM_WIDTH, + g_param_spec_double ( + "minimum_width", + "Minimum width", + "Minimum Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_FROZEN, + g_param_spec_boolean ( + "frozen", + "Frozen", + "Frozen", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_UNIFORM_ROW_HEIGHT, + g_param_spec_boolean ( + "uniform_row_height", + "Uniform row height", + "Uniform row height", + FALSE, + G_PARAM_READWRITE)); +} + +static void +etgl_init (ETableGroupLeaf *etgl) +{ + etgl->width = 1; + etgl->height = 1; + etgl->minimum_width = 0; + + etgl->ets = NULL; + etgl->item = NULL; + + etgl->etgl_cursor_change_id = 0; + etgl->etgl_cursor_activated_id = 0; + etgl->etgl_double_click_id = 0; + etgl->etgl_right_click_id = 0; + etgl->etgl_click_id = 0; + etgl->etgl_key_press_id = 0; + etgl->etgl_start_drag_id = 0; + + etgl->alternating_row_colors = 1; + etgl->horizontal_draw_grid = 1; + etgl->vertical_draw_grid = 1; + etgl->draw_focus = 1; + etgl->cursor_mode = E_CURSOR_SIMPLE; + etgl->length_threshold = -1; + + etgl->selection_model = NULL; + etgl->uniform_row_height = FALSE; + + e_canvas_item_set_reflow_callback (GNOME_CANVAS_ITEM (etgl), etgl_reflow); +} + diff --git a/e-util/e-table-group-leaf.h b/e-util/e-table-group-leaf.h new file mode 100644 index 0000000000..93aa2bf2da --- /dev/null +++ b/e-util/e-table-group-leaf.h @@ -0,0 +1,110 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_GROUP_LEAF_H_ +#define _E_TABLE_GROUP_LEAF_H_ + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-table-group.h> +#include <e-util/e-table-item.h> +#include <e-util/e-table-subset.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_GROUP_LEAF \ + (e_table_group_leaf_get_type ()) +#define E_TABLE_GROUP_LEAF(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_GROUP_LEAF, ETableGroupLeaf)) +#define E_TABLE_GROUP_LEAF_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_GROUP_LEAF, ETableGroupLeafClass)) +#define E_IS_TABLE_GROUP_LEAF(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_GROUP_LEAF)) +#define E_IS_TABLE_GROUP_LEAF_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_GROUP_LEAF)) +#define E_TABLE_GROUP_LEAF_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_GROUP_LEAF, ETableGroupLeafClass)) + +G_BEGIN_DECLS + +typedef struct _ETableGroupLeaf ETableGroupLeaf; +typedef struct _ETableGroupLeafClass ETableGroupLeafClass; + +struct _ETableGroupLeaf { + ETableGroup group; + + /* + * Item. + */ + ETableItem *item; + + gdouble height; + gdouble width; + gdouble minimum_width; + + gint length_threshold; + + ETableSubset *ets; + guint is_grouped : 1; + + guint alternating_row_colors : 1; + guint horizontal_draw_grid : 1; + guint vertical_draw_grid : 1; + guint draw_focus : 1; + guint uniform_row_height : 1; + ECursorMode cursor_mode; + + gint etgl_cursor_change_id; + gint etgl_cursor_activated_id; + gint etgl_double_click_id; + gint etgl_right_click_id; + gint etgl_click_id; + gint etgl_key_press_id; + gint etgl_start_drag_id; + + ESelectionModel *selection_model; +}; + +struct _ETableGroupLeafClass { + ETableGroupClass parent_class; +}; + +GType e_table_group_leaf_get_type (void) G_GNUC_CONST; +ETableGroup * e_table_group_leaf_new (GnomeCanvasGroup *parent, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model, + ETableSortInfo *sort_info); + +G_END_DECLS + +#endif /* _E_TABLE_GROUP_LEAF_H_ */ + diff --git a/e-util/e-table-group.c b/e-util/e-table-group.c new file mode 100644 index 0000000000..b119b06982 --- /dev/null +++ b/e-util/e-table-group.c @@ -0,0 +1,771 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-table-group.h" +#include "e-table-group-container.h" +#include "e-table-group-leaf.h" +#include "e-table-item.h" + +/* workaround for avoiding API breakage*/ +#define etg_get_type e_table_group_get_type +G_DEFINE_TYPE (ETableGroup, etg, GNOME_TYPE_CANVAS_GROUP) + +#define ETG_CLASS(e) (E_TABLE_GROUP_CLASS(G_OBJECT_GET_CLASS(e))) + +enum { + CURSOR_CHANGE, + CURSOR_ACTIVATED, + DOUBLE_CLICK, + RIGHT_CLICK, + CLICK, + KEY_PRESS, + START_DRAG, + LAST_SIGNAL +}; + +static guint etg_signals[LAST_SIGNAL] = { 0, }; + +static gboolean etg_get_focus (ETableGroup *etg); + +static void +etg_dispose (GObject *object) +{ + ETableGroup *etg = E_TABLE_GROUP (object); + + if (etg->header) { + g_object_unref (etg->header); + etg->header = NULL; + } + + if (etg->full_header) { + g_object_unref (etg->full_header); + etg->full_header = NULL; + } + + if (etg->model) { + g_object_unref (etg->model); + etg->model = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (etg_parent_class)->dispose (object); +} + +/** + * e_table_group_new + * @parent: The %GnomeCanvasGroup to create a child of. + * @full_header: The full header of the %ETable. + * @header: The current header of the %ETable. + * @model: The %ETableModel of the %ETable. + * @sort_info: The %ETableSortInfo of the %ETable. + * @n: The grouping information object to group by. + * + * %ETableGroup is a collection of rows of an %ETable. It's a + * %GnomeCanvasItem. There are two different forms. If n < the + * number of groupings in the given %ETableSortInfo, then the + * %ETableGroup will need to contain other %ETableGroups, thus it + * creates an %ETableGroupContainer. Otherwise, it will just contain + * an %ETableItem, and thus it creates an %ETableGroupLeaf. + * + * Returns: The new %ETableGroup. + */ +ETableGroup * +e_table_group_new (GnomeCanvasGroup *parent, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model, + ETableSortInfo *sort_info, + gint n) +{ + g_return_val_if_fail (model != NULL, NULL); + + if (n < e_table_sort_info_grouping_get_count (sort_info)) { + return e_table_group_container_new ( + parent, full_header, header, model, sort_info, n); + } else { + return e_table_group_leaf_new ( + parent, full_header, header, model, sort_info); + } +} + +/** + * e_table_group_construct + * @parent: The %GnomeCanvasGroup to create a child of. + * @etg: The %ETableGroup to construct. + * @full_header: The full header of the %ETable. + * @header: The current header of the %ETable. + * @model: The %ETableModel of the %ETable. + * + * This routine does the base construction of the %ETableGroup. + */ +void +e_table_group_construct (GnomeCanvasGroup *parent, + ETableGroup *etg, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model) +{ + etg->full_header = full_header; + g_object_ref (etg->full_header); + etg->header = header; + g_object_ref (etg->header); + etg->model = model; + g_object_ref (etg->model); + g_object_set (etg, "parent", parent, NULL); +} + +/** + * e_table_group_add + * @etg: The %ETableGroup to add a row to + * @row: The row to add. + * + * This routine adds the given row from the %ETableModel to this set + * of rows. + */ +void +e_table_group_add (ETableGroup *etg, + gint row) +{ + g_return_if_fail (etg != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (etg)); + + g_return_if_fail (ETG_CLASS (etg)->add != NULL); + ETG_CLASS (etg)->add (etg, row); +} + +/** + * e_table_group_add_array + * @etg: The %ETableGroup to add to + * @array: The array to add. + * @count: The number of times to add + * + * This routine adds all the rows in the array to this set of rows. + * It assumes that the array is already sorted properly. + */ +void +e_table_group_add_array (ETableGroup *etg, + const gint *array, + gint count) +{ + g_return_if_fail (etg != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (etg)); + + g_return_if_fail (ETG_CLASS (etg)->add_array != NULL); + ETG_CLASS (etg)->add_array (etg, array, count); +} + +/** + * e_table_group_add_all + * @etg: The %ETableGroup to add to + * + * This routine adds all the rows from the %ETableModel to this set + * of rows. + */ +void +e_table_group_add_all (ETableGroup *etg) +{ + g_return_if_fail (etg != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (etg)); + + g_return_if_fail (ETG_CLASS (etg)->add_all != NULL); + ETG_CLASS (etg)->add_all (etg); +} + +/** + * e_table_group_remove + * @etg: The %ETableGroup to remove a row from + * @row: The row to remove. + * + * This routine removes the given row from the %ETableModel from this + * set of rows. + * + * Returns: TRUE if the row was deleted and FALSE if the row was not + * found. + */ +gboolean +e_table_group_remove (ETableGroup *etg, + gint row) +{ + g_return_val_if_fail (etg != NULL, FALSE); + g_return_val_if_fail (E_IS_TABLE_GROUP (etg), FALSE); + + g_return_val_if_fail (ETG_CLASS (etg)->remove != NULL, FALSE); + return ETG_CLASS (etg)->remove (etg, row); +} + +/** + * e_table_group_increment + * @etg: The %ETableGroup to increment + * @position: The position to increment from + * @amount: The amount to increment. + * + * This routine adds amount to all rows greater than or equal to + * position. This is to handle when a row gets inserted into the + * model. + */ +void +e_table_group_increment (ETableGroup *etg, + gint position, + gint amount) +{ + g_return_if_fail (etg != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (etg)); + + g_return_if_fail (ETG_CLASS (etg)->increment != NULL); + ETG_CLASS (etg)->increment (etg, position, amount); +} + +/** + * e_table_group_increment + * @etg: The %ETableGroup to decrement + * @position: The position to decrement from + * @amount: The amount to decrement + * + * This routine removes amount from all rows greater than or equal to + * position. This is to handle when a row gets deleted from the + * model. + */ +void +e_table_group_decrement (ETableGroup *etg, + gint position, + gint amount) +{ + g_return_if_fail (etg != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (etg)); + + g_return_if_fail (ETG_CLASS (etg)->decrement != NULL); + ETG_CLASS (etg)->decrement (etg, position, amount); +} + +/** + * e_table_group_increment + * @etg: The %ETableGroup to count + * + * This routine calculates the number of rows shown in this group. + * + * Returns: The number of rows. + */ +gint +e_table_group_row_count (ETableGroup *etg) +{ + g_return_val_if_fail (etg != NULL, 0); + g_return_val_if_fail (E_IS_TABLE_GROUP (etg), -1); + + g_return_val_if_fail (ETG_CLASS (etg)->row_count != NULL, -1); + return ETG_CLASS (etg)->row_count (etg); +} + +/** + * e_table_group_set_focus + * @etg: The %ETableGroup to set + * @direction: The direction the focus is coming from. + * @view_col: The column to set the focus in. + * + * Sets the focus to this widget. Places the focus in the view column + * coming from direction direction. + */ +void +e_table_group_set_focus (ETableGroup *etg, + EFocus direction, + gint view_col) +{ + g_return_if_fail (etg != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (etg)); + + g_return_if_fail (ETG_CLASS (etg)->set_focus != NULL); + ETG_CLASS (etg)->set_focus (etg, direction, view_col); +} + +/** + * e_table_group_get_focus + * @etg: The %ETableGroup to check + * + * Calculates if this group has the focus. + * + * Returns: TRUE if this group has the focus. + */ +gboolean +e_table_group_get_focus (ETableGroup *etg) +{ + g_return_val_if_fail (etg != NULL, FALSE); + g_return_val_if_fail (E_IS_TABLE_GROUP (etg), FALSE); + + g_return_val_if_fail (ETG_CLASS (etg)->get_focus != NULL, FALSE); + return ETG_CLASS (etg)->get_focus (etg); +} + +/** + * e_table_group_get_focus_column + * @etg: The %ETableGroup to check + * + * Calculates which column in this group has the focus. + * + * Returns: The column index (view column). + */ +gint +e_table_group_get_focus_column (ETableGroup *etg) +{ + g_return_val_if_fail (etg != NULL, -1); + g_return_val_if_fail (E_IS_TABLE_GROUP (etg), -1); + + g_return_val_if_fail (ETG_CLASS (etg)->get_focus_column != NULL, -1); + return ETG_CLASS (etg)->get_focus_column (etg); +} + +/** + * e_table_group_get_printable + * @etg: %ETableGroup which will be printed + * + * This routine creates and returns an %EPrintable that can be used to + * print the given %ETableGroup. + * + * Returns: The %EPrintable. + */ +EPrintable * +e_table_group_get_printable (ETableGroup *etg) +{ + g_return_val_if_fail (etg != NULL, NULL); + g_return_val_if_fail (E_IS_TABLE_GROUP (etg), NULL); + + g_return_val_if_fail (ETG_CLASS (etg)->get_printable != NULL, NULL); + return ETG_CLASS (etg)->get_printable (etg); +} + +/** + * e_table_group_compute_location + * @eti: %ETableGroup to look in. + * @x: A pointer to the x location to find in the %ETableGroup. + * @y: A pointer to the y location to find in the %ETableGroup. + * @row: A pointer to the location to store the found row in. + * @col: A pointer to the location to store the found col in. + * + * This routine locates the pixel location (*x, *y) in the + * %ETableGroup. If that location is in the %ETableGroup, *row and + * *col are set to the view row and column where it was found. If + * that location is not in the %ETableGroup, the height of the + * %ETableGroup is removed from the value y points to. + */ +void +e_table_group_compute_location (ETableGroup *etg, + gint *x, + gint *y, + gint *row, + gint *col) +{ + g_return_if_fail (etg != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (etg)); + + g_return_if_fail (ETG_CLASS (etg)->compute_location != NULL); + ETG_CLASS (etg)->compute_location (etg, x, y, row, col); +} + +void +e_table_group_get_mouse_over (ETableGroup *etg, + gint *row, + gint *col) +{ + g_return_if_fail (etg != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (etg)); + + g_return_if_fail (ETG_CLASS (etg)->get_mouse_over != NULL); + ETG_CLASS (etg)->get_mouse_over (etg, row, col); +} + +/** + * e_table_group_get_position + * @eti: %ETableGroup to look in. + * @x: A pointer to the location to store the found x location in. + * @y: A pointer to the location to store the found y location in. + * @row: A pointer to the row number to find. + * @col: A pointer to the col number to find. + * + * This routine finds the view cell (row, col) in the #ETableGroup. + * If that location is in the #ETableGroup *@x and *@y are set to the + * upper left hand corner of the cell found. If that location is not + * in the #ETableGroup, the number of rows in the #ETableGroup is + * removed from the value row points to. + */ +void +e_table_group_get_cell_geometry (ETableGroup *etg, + gint *row, + gint *col, + gint *x, + gint *y, + gint *width, + gint *height) +{ + g_return_if_fail (etg != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (etg)); + + g_return_if_fail (ETG_CLASS (etg)->get_cell_geometry != NULL); + ETG_CLASS (etg)->get_cell_geometry (etg, row, col, x, y, width, height); +} + +/** + * e_table_group_cursor_change + * @eti: %ETableGroup to emit the signal on + * @row: The new cursor row (model row) + * + * This routine emits the "cursor_change" signal. + */ +void +e_table_group_cursor_change (ETableGroup *e_table_group, + gint row) +{ + g_return_if_fail (e_table_group != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (e_table_group)); + + g_signal_emit ( + e_table_group, + etg_signals[CURSOR_CHANGE], 0, + row); +} + +/** + * e_table_group_cursor_activated + * @eti: %ETableGroup to emit the signal on + * @row: The cursor row (model row) + * + * This routine emits the "cursor_activated" signal. + */ +void +e_table_group_cursor_activated (ETableGroup *e_table_group, + gint row) +{ + g_return_if_fail (e_table_group != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (e_table_group)); + + g_signal_emit ( + e_table_group, + etg_signals[CURSOR_ACTIVATED], 0, + row); +} + +/** + * e_table_group_double_click + * @eti: %ETableGroup to emit the signal on + * @row: The row clicked on (model row) + * @col: The col clicked on (model col) + * @event: The event that caused this signal + * + * This routine emits the "double_click" signal. + */ +void +e_table_group_double_click (ETableGroup *e_table_group, + gint row, + gint col, + GdkEvent *event) +{ + g_return_if_fail (e_table_group != NULL); + g_return_if_fail (E_IS_TABLE_GROUP (e_table_group)); + + g_signal_emit ( + e_table_group, + etg_signals[DOUBLE_CLICK], 0, + row, col, event); +} + +/** + * e_table_group_right_click + * @eti: %ETableGroup to emit the signal on + * @row: The row clicked on (model row) + * @col: The col clicked on (model col) + * @event: The event that caused this signal + * + * This routine emits the "right_click" signal. + */ +gboolean +e_table_group_right_click (ETableGroup *e_table_group, + gint row, + gint col, + GdkEvent *event) +{ + gboolean return_val = FALSE; + + g_return_val_if_fail (e_table_group != NULL, FALSE); + g_return_val_if_fail (E_IS_TABLE_GROUP (e_table_group), FALSE); + + g_signal_emit ( + e_table_group, + etg_signals[RIGHT_CLICK], 0, + row, col, event, &return_val); + + return return_val; +} + +/** + * e_table_group_click + * @eti: %ETableGroup to emit the signal on + * @row: The row clicked on (model row) + * @col: The col clicked on (model col) + * @event: The event that caused this signal + * + * This routine emits the "click" signal. + */ +gboolean +e_table_group_click (ETableGroup *e_table_group, + gint row, + gint col, + GdkEvent *event) +{ + gboolean return_val = FALSE; + + g_return_val_if_fail (e_table_group != NULL, FALSE); + g_return_val_if_fail (E_IS_TABLE_GROUP (e_table_group), FALSE); + + g_signal_emit ( + e_table_group, + etg_signals[CLICK], 0, + row, col, event, &return_val); + + return return_val; +} + +/** + * e_table_group_key_press + * @eti: %ETableGroup to emit the signal on + * @row: The cursor row (model row) + * @col: The cursor col (model col) + * @event: The event that caused this signal + * + * This routine emits the "key_press" signal. + */ +gboolean +e_table_group_key_press (ETableGroup *e_table_group, + gint row, + gint col, + GdkEvent *event) +{ + gboolean return_val = FALSE; + + g_return_val_if_fail (e_table_group != NULL, FALSE); + g_return_val_if_fail (E_IS_TABLE_GROUP (e_table_group), FALSE); + + g_signal_emit ( + e_table_group, + etg_signals[KEY_PRESS], 0, + row, col, event, &return_val); + + return return_val; +} + +/** + * e_table_group_start_drag + * @eti: %ETableGroup to emit the signal on + * @row: The cursor row (model row) + * @col: The cursor col (model col) + * @event: The event that caused this signal + * + * This routine emits the "start_drag" signal. + */ +gboolean +e_table_group_start_drag (ETableGroup *e_table_group, + gint row, + gint col, + GdkEvent *event) +{ + gboolean return_val = FALSE; + + g_return_val_if_fail (e_table_group != NULL, FALSE); + g_return_val_if_fail (E_IS_TABLE_GROUP (e_table_group), FALSE); + + g_signal_emit ( + e_table_group, + etg_signals[START_DRAG], 0, + row, col, event, &return_val); + + return return_val; +} + +/** + * e_table_group_get_header + * @eti: %ETableGroup to check + * + * This routine returns the %ETableGroup's header. + * + * Returns: The %ETableHeader. + */ +ETableHeader * +e_table_group_get_header (ETableGroup *etg) +{ + g_return_val_if_fail (etg != NULL, NULL); + g_return_val_if_fail (E_IS_TABLE_GROUP (etg), NULL); + + return etg->header; +} + +static gint +etg_event (GnomeCanvasItem *item, + GdkEvent *event) +{ + ETableGroup *etg = E_TABLE_GROUP (item); + gboolean return_val = TRUE; + + switch (event->type) { + + case GDK_FOCUS_CHANGE: + etg->has_focus = event->focus_change.in; + return_val = FALSE; + break; + + default: + return_val = FALSE; + } + if (return_val == FALSE) { + if (GNOME_CANVAS_ITEM_CLASS (etg_parent_class)->event) + return GNOME_CANVAS_ITEM_CLASS (etg_parent_class)->event (item, event); + } + return return_val; + +} + +static gboolean +etg_get_focus (ETableGroup *etg) +{ + return etg->has_focus; +} + +static void +etg_class_init (ETableGroupClass *class) +{ + GnomeCanvasItemClass *item_class = GNOME_CANVAS_ITEM_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = etg_dispose; + + item_class->event = etg_event; + + class->cursor_change = NULL; + class->cursor_activated = NULL; + class->double_click = NULL; + class->right_click = NULL; + class->click = NULL; + class->key_press = NULL; + class->start_drag = NULL; + + class->add = NULL; + class->add_array = NULL; + class->add_all = NULL; + class->remove = NULL; + class->row_count = NULL; + class->increment = NULL; + class->decrement = NULL; + class->set_focus = NULL; + class->get_focus = etg_get_focus; + class->get_printable = NULL; + class->compute_location = NULL; + class->get_mouse_over = NULL; + class->get_cell_geometry = NULL; + + etg_signals[CURSOR_CHANGE] = g_signal_new ( + "cursor_change", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableGroupClass, cursor_change), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + etg_signals[CURSOR_ACTIVATED] = g_signal_new ( + "cursor_activated", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableGroupClass, cursor_activated), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + etg_signals[DOUBLE_CLICK] = g_signal_new ( + "double_click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableGroupClass, double_click), + NULL, NULL, + e_marshal_NONE__INT_INT_BOXED, + G_TYPE_NONE, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + etg_signals[RIGHT_CLICK] = g_signal_new ( + "right_click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableGroupClass, right_click), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + etg_signals[CLICK] = g_signal_new ( + "click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableGroupClass, click), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + etg_signals[KEY_PRESS] = g_signal_new ( + "key_press", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableGroupClass, key_press), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + etg_signals[START_DRAG] = g_signal_new ( + "start_drag", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableGroupClass, start_drag), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); +} + +static void +etg_init (ETableGroup *etg) +{ + /* nothing to do */ +} diff --git a/e-util/e-table-group.h b/e-util/e-table-group.h new file mode 100644 index 0000000000..7e9e905753 --- /dev/null +++ b/e-util/e-table-group.h @@ -0,0 +1,242 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_GROUP_H_ +#define _E_TABLE_GROUP_H_ + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-misc-utils.h> +#include <e-util/e-printable.h> +#include <e-util/e-table-defines.h> +#include <e-util/e-table-header.h> +#include <e-util/e-table-model.h> +#include <e-util/e-table-sort-info.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_GROUP \ + (e_table_group_get_type ()) +#define E_TABLE_GROUP(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_GROUP, ETableGroup)) +#define E_TABLE_GROUP_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_GROUP, ETableGroupClass)) +#define E_IS_TABLE_GROUP(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_GROUP)) +#define E_IS_TABLE_GROUP_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_GROUP)) +#define E_TABLE_GROUP_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_GROUP, ETableGroupClass)) + +G_BEGIN_DECLS + +typedef struct _ETableGroup ETableGroup; +typedef struct _ETableGroupClass ETableGroupClass; + +struct _ETableGroup { + GnomeCanvasGroup group; + + /* + * The full header. + */ + ETableHeader *full_header; + ETableHeader *header; + + /* + * The model we pull data from. + */ + ETableModel *model; + + /* + * Whether we should add indentation and open/close markers, + * or if we just act as containers of subtables. + */ + guint transparent : 1; + + guint has_focus : 1; + + guint frozen : 1; +}; + +struct _ETableGroupClass { + GnomeCanvasGroupClass parent_class; + + /* Signals */ + void (*cursor_change) (ETableGroup *etg, + gint row); + void (*cursor_activated) (ETableGroup *etg, + gint row); + void (*double_click) (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); + gboolean (*right_click) (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); + gboolean (*click) (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); + gboolean (*key_press) (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); + gint (*start_drag) (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); + + /* Virtual functions. */ + void (*add) (ETableGroup *etg, + gint row); + void (*add_array) (ETableGroup *etg, + const gint *array, + gint count); + void (*add_all) (ETableGroup *etg); + gboolean (*remove) (ETableGroup *etg, + gint row); + gint (*row_count) (ETableGroup *etg); + void (*increment) (ETableGroup *etg, + gint position, + gint amount); + void (*decrement) (ETableGroup *etg, + gint position, + gint amount); + void (*set_focus) (ETableGroup *etg, + EFocus direction, + gint view_col); + gboolean (*get_focus) (ETableGroup *etg); + gint (*get_focus_column) (ETableGroup *etg); + EPrintable * (*get_printable) (ETableGroup *etg); + void (*compute_location) (ETableGroup *etg, + gint *x, + gint *y, + gint *row, + gint *col); + void (*get_mouse_over) (ETableGroup *etg, + gint *row, + gint *col); + void (*get_cell_geometry) (ETableGroup *etg, + gint *row, + gint *col, + gint *x, + gint *y, + gint *width, + gint *height); +}; + +GType e_table_group_get_type (void) G_GNUC_CONST; +ETableGroup * e_table_group_new (GnomeCanvasGroup *parent, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model, + ETableSortInfo *sort_info, + gint n); +void e_table_group_construct (GnomeCanvasGroup *parent, + ETableGroup *etg, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model); + +/* Virtual functions */ +void e_table_group_add (ETableGroup *etg, + gint row); +void e_table_group_add_array (ETableGroup *etg, + const gint *array, + gint count); +void e_table_group_add_all (ETableGroup *etg); +gboolean e_table_group_remove (ETableGroup *etg, + gint row); +void e_table_group_increment (ETableGroup *etg, + gint position, + gint amount); +void e_table_group_decrement (ETableGroup *etg, + gint position, + gint amount); +gint e_table_group_row_count (ETableGroup *etg); +void e_table_group_set_focus (ETableGroup *etg, + EFocus direction, + gint view_col); +gboolean e_table_group_get_focus (ETableGroup *etg); +gint e_table_group_get_focus_column (ETableGroup *etg); +ETableHeader * e_table_group_get_header (ETableGroup *etg); +EPrintable * e_table_group_get_printable (ETableGroup *etg); +void e_table_group_compute_location (ETableGroup *etg, + gint *x, + gint *y, + gint *row, + gint *col); +void e_table_group_get_mouse_over (ETableGroup *etg, + gint *row, + gint *col); +void e_table_group_get_cell_geometry (ETableGroup *etg, + gint *row, + gint *col, + gint *x, + gint *y, + gint *width, + gint *height); + +/* For emitting the signals */ +void e_table_group_cursor_change (ETableGroup *etg, + gint row); +void e_table_group_cursor_activated (ETableGroup *etg, + gint row); +void e_table_group_double_click (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); +gboolean e_table_group_right_click (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); +gboolean e_table_group_click (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); +gboolean e_table_group_key_press (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); +gint e_table_group_start_drag (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event); + +typedef void (*ETableGroupLeafFn) (gpointer e_table_item, gpointer closure); +void e_table_group_apply_to_leafs (ETableGroup *etg, + ETableGroupLeafFn fn, + gpointer closure); + +G_END_DECLS + +#endif /* _E_TABLE_GROUP_H_ */ diff --git a/e-util/e-table-header-item.c b/e-util/e-table-header-item.c new file mode 100644 index 0000000000..103ed3a807 --- /dev/null +++ b/e-util/e-table-header-item.c @@ -0,0 +1,2226 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@gnu.org> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-header-item.h" + +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-canvas.h" +#include "e-popup-menu.h" +#include "e-table-col-dnd.h" +#include "e-table-config.h" +#include "e-table-defines.h" +#include "e-table-field-chooser-dialog.h" +#include "e-table-header-utils.h" +#include "e-table-header.h" +#include "e-table.h" +#include "e-xml-utils.h" + +#include "arrow-up.xpm" +#include "arrow-down.xpm" + +enum { + BUTTON_PRESSED, + LAST_SIGNAL +}; + +static guint ethi_signals[LAST_SIGNAL] = { 0, }; + +#define ARROW_DOWN_HEIGHT 16 +#define ARROW_PTR 7 + +/* Defines the tolerance for proximity of the column division to the cursor position */ +#define TOLERANCE 4 + +#define ETHI_RESIZING(x) ((x)->resize_col != -1) + +#define ethi_get_type e_table_header_item_get_type +G_DEFINE_TYPE (ETableHeaderItem, ethi, GNOME_TYPE_CANVAS_ITEM) + +#define d(x) + +static void ethi_drop_table_header (ETableHeaderItem *ethi); + +/* + * They display the arrows for the drop location. + */ + +static GtkWidget *arrow_up, *arrow_down; + +enum { + PROP_0, + PROP_TABLE_HEADER, + PROP_FULL_HEADER, + PROP_DND_CODE, + PROP_TABLE_FONT_DESC, + PROP_SORT_INFO, + PROP_TABLE, + PROP_TREE +}; + +enum { + ET_SCROLL_UP = 1 << 0, + ET_SCROLL_DOWN = 1 << 1, + ET_SCROLL_LEFT = 1 << 2, + ET_SCROLL_RIGHT = 1 << 3 +}; + +static void scroll_off (ETableHeaderItem *ethi); +static void scroll_on (ETableHeaderItem *ethi, guint scroll_direction); + +static void +ethi_dispose (GObject *object) +{ + ETableHeaderItem *ethi = E_TABLE_HEADER_ITEM (object); + + ethi_drop_table_header (ethi); + + scroll_off (ethi); + + if (ethi->resize_cursor) { + g_object_unref (ethi->resize_cursor); + ethi->resize_cursor = NULL; + } + + if (ethi->dnd_code) { + g_free (ethi->dnd_code); + ethi->dnd_code = NULL; + } + + if (ethi->sort_info) { + if (ethi->sort_info_changed_id) + g_signal_handler_disconnect ( + ethi->sort_info, ethi->sort_info_changed_id); + if (ethi->group_info_changed_id) + g_signal_handler_disconnect ( + ethi->sort_info, ethi->group_info_changed_id); + g_object_unref (ethi->sort_info); + ethi->sort_info = NULL; + } + + if (ethi->full_header) + g_object_unref (ethi->full_header); + ethi->full_header = NULL; + + if (ethi->etfcd.widget) + g_object_remove_weak_pointer ( + G_OBJECT (ethi->etfcd.widget), ði->etfcd.pointer); + + if (ethi->config) + g_object_unref (ethi->config); + ethi->config = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (ethi_parent_class)->dispose (object); +} + +static gint +e_table_header_item_get_height (ETableHeaderItem *ethi) +{ + ETableHeader *eth; + gint numcols, col; + gint maxheight; + + g_return_val_if_fail (ethi != NULL, 0); + g_return_val_if_fail (E_IS_TABLE_HEADER_ITEM (ethi), 0); + + eth = ethi->eth; + numcols = e_table_header_count (eth); + + maxheight = 0; + + for (col = 0; col < numcols; col++) { + ETableCol *ecol = e_table_header_get_column (eth, col); + gint height; + + height = e_table_header_compute_height ( + ecol, GTK_WIDGET (GNOME_CANVAS_ITEM (ethi)->canvas)); + + if (height > maxheight) + maxheight = height; + } + + return maxheight; +} + +static void +ethi_update (GnomeCanvasItem *item, + const cairo_matrix_t *i2c, + gint flags) +{ + ETableHeaderItem *ethi = E_TABLE_HEADER_ITEM (item); + gdouble x1, y1, x2, y2; + + if (GNOME_CANVAS_ITEM_CLASS (ethi_parent_class)->update) + GNOME_CANVAS_ITEM_CLASS (ethi_parent_class)->update ( + item, i2c, flags); + + if (ethi->sort_info) + ethi->group_indent_width = + e_table_sort_info_grouping_get_count (ethi->sort_info) + * GROUP_INDENT; + else + ethi->group_indent_width = 0; + + ethi->width = + e_table_header_total_width (ethi->eth) + + ethi->group_indent_width; + + x1 = y1 = 0; + x2 = ethi->width; + y2 = ethi->height; + + gnome_canvas_matrix_transform_rect (i2c, &x1, &y1, &x2, &y2); + + if (item->x1 != x1 || + item->y1 != y1 || + item->x2 != x2 || + item->y2 != y2) { + gnome_canvas_request_redraw ( + item->canvas, + item->x1, item->y1, + item->x2, item->y2); + item->x1 = x1; + item->y1 = y1; + item->x2 = x2; + item->y2 = y2; + } + gnome_canvas_request_redraw ( + item->canvas, item->x1, item->y1, item->x2, item->y2); +} + +static void +ethi_font_set (ETableHeaderItem *ethi, + PangoFontDescription *font_desc) +{ + if (ethi->font_desc) + pango_font_description_free (ethi->font_desc); + + ethi->font_desc = pango_font_description_copy (font_desc); + + ethi->height = e_table_header_item_get_height (ethi); + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (ethi)); +} + +static void +ethi_drop_table_header (ETableHeaderItem *ethi) +{ + GObject *header; + + if (!ethi->eth) + return; + + header = G_OBJECT (ethi->eth); + g_signal_handler_disconnect (header, ethi->structure_change_id); + g_signal_handler_disconnect (header, ethi->dimension_change_id); + + g_object_unref (header); + ethi->eth = NULL; + ethi->width = 0; +} + +static void +structure_changed (ETableHeader *header, + ETableHeaderItem *ethi) +{ + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ethi)); +} + +static void +dimension_changed (ETableHeader *header, + gint col, + ETableHeaderItem *ethi) +{ + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ethi)); +} + +static void +ethi_add_table_header (ETableHeaderItem *ethi, + ETableHeader *header) +{ + ethi->eth = header; + g_object_ref (ethi->eth); + + ethi->height = e_table_header_item_get_height (ethi); + + ethi->structure_change_id = g_signal_connect ( + header, "structure_change", + G_CALLBACK (structure_changed), ethi); + ethi->dimension_change_id = g_signal_connect ( + header, "dimension_change", + G_CALLBACK (dimension_changed), ethi); + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (ethi)); + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ethi)); +} + +static void +ethi_sort_info_changed (ETableSortInfo *sort_info, + ETableHeaderItem *ethi) +{ + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ethi)); +} + +static void +ethi_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GnomeCanvasItem *item; + ETableHeaderItem *ethi; + + item = GNOME_CANVAS_ITEM (object); + ethi = E_TABLE_HEADER_ITEM (object); + + switch (property_id) { + case PROP_TABLE_HEADER: + ethi_drop_table_header (ethi); + ethi_add_table_header (ethi, E_TABLE_HEADER (g_value_get_object (value))); + break; + + case PROP_FULL_HEADER: + if (ethi->full_header) + g_object_unref (ethi->full_header); + ethi->full_header = E_TABLE_HEADER (g_value_get_object (value)); + if (ethi->full_header) + g_object_ref (ethi->full_header); + break; + + case PROP_DND_CODE: + g_free (ethi->dnd_code); + ethi->dnd_code = g_strdup (g_value_get_string (value)); + break; + + case PROP_TABLE_FONT_DESC: + ethi_font_set (ethi, g_value_get_boxed (value)); + break; + + case PROP_SORT_INFO: + if (ethi->sort_info) { + if (ethi->sort_info_changed_id) + g_signal_handler_disconnect ( + ethi->sort_info, + ethi->sort_info_changed_id); + + if (ethi->group_info_changed_id) + g_signal_handler_disconnect ( + ethi->sort_info, + ethi->group_info_changed_id); + g_object_unref (ethi->sort_info); + } + ethi->sort_info = g_value_get_object (value); + g_object_ref (ethi->sort_info); + ethi->sort_info_changed_id = + g_signal_connect ( + ethi->sort_info, "sort_info_changed", + G_CALLBACK (ethi_sort_info_changed), ethi); + ethi->group_info_changed_id = + g_signal_connect ( + ethi->sort_info, "group_info_changed", + G_CALLBACK (ethi_sort_info_changed), ethi); + break; + case PROP_TABLE: + if (g_value_get_object (value)) + ethi->table = E_TABLE (g_value_get_object (value)); + else + ethi->table = NULL; + break; + case PROP_TREE: + if (g_value_get_object (value)) + ethi->tree = E_TREE (g_value_get_object (value)); + else + ethi->tree = NULL; + break; + } + gnome_canvas_item_request_update (item); +} + +static void +ethi_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableHeaderItem *ethi; + + ethi = E_TABLE_HEADER_ITEM (object); + + switch (property_id) { + case PROP_FULL_HEADER: + g_value_set_object (value, ethi->full_header); + break; + case PROP_DND_CODE: + g_value_set_string (value, ethi->dnd_code); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static gint +ethi_find_col_by_x (ETableHeaderItem *ethi, + gint x) +{ + const gint cols = e_table_header_count (ethi->eth); + gint x1 = 0; + gint col; + + d (g_print ("%s:%d: x = %d, x1 = %d\n", __FUNCTION__, __LINE__, x, x1)); + + x1 += ethi->group_indent_width; + + if (x < x1) { + d (g_print ("%s:%d: Returning 0\n", __FUNCTION__, __LINE__)); + return 0; + } + + for (col = 0; col < cols; col++) { + ETableCol *ecol = e_table_header_get_column (ethi->eth, col); + + if ((x >= x1) && (x <= x1 + ecol->width)) { + d (g_print ("%s:%d: Returning %d\n", __FUNCTION__, __LINE__, col)); + return col; + } + + x1 += ecol->width; + } + d (g_print ("%s:%d: Returning %d\n", __FUNCTION__, __LINE__, cols - 1)); + return cols - 1; +} + +static gint +ethi_find_col_by_x_nearest (ETableHeaderItem *ethi, + gint x) +{ + const gint cols = e_table_header_count (ethi->eth); + gint x1 = 0; + gint col; + + x1 += ethi->group_indent_width; + + if (x < x1) + return 0; + + for (col = 0; col < cols; col++) { + ETableCol *ecol = e_table_header_get_column (ethi->eth, col); + + x1 += (ecol->width / 2); + + if (x <= x1) + return col; + + x1 += (ecol->width + 1) / 2; + } + return col; +} + +static void +ethi_remove_drop_marker (ETableHeaderItem *ethi) +{ + if (ethi->drag_mark == -1) + return; + + gtk_widget_hide (arrow_up); + gtk_widget_hide (arrow_down); + + ethi->drag_mark = -1; +} + +static GtkWidget * +make_shaped_window_from_xpm (const gchar **xpm) +{ + GdkPixbuf *pixbuf; + GtkWidget *win, *pix; + + pixbuf = gdk_pixbuf_new_from_xpm_data (xpm); + + win = gtk_window_new (GTK_WINDOW_POPUP); + gtk_window_set_type_hint (GTK_WINDOW (win), GDK_WINDOW_TYPE_HINT_NOTIFICATION); + + pix = gtk_image_new_from_pixbuf (pixbuf); + gtk_widget_realize (win); + gtk_container_add (GTK_CONTAINER (win), pix); + + g_object_unref (pixbuf); + + return win; +} + +static void +ethi_add_drop_marker (ETableHeaderItem *ethi, + gint col, + gboolean recreate) +{ + GnomeCanvas *canvas; + GtkAdjustment *adjustment; + GdkWindow *window; + gint rx, ry; + gint x; + + if (!recreate && ethi->drag_mark == col) + return; + + ethi->drag_mark = col; + + x = e_table_header_col_diff (ethi->eth, 0, col); + if (col > 0) + x += ethi->group_indent_width; + + if (!arrow_up) { + arrow_up = make_shaped_window_from_xpm (arrow_up_xpm); + arrow_down = make_shaped_window_from_xpm (arrow_down_xpm); + } + + canvas = GNOME_CANVAS_ITEM (ethi)->canvas; + window = gtk_widget_get_window (GTK_WIDGET (canvas)); + gdk_window_get_origin (window, &rx, &ry); + + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (canvas)); + rx -= gtk_adjustment_get_value (adjustment); + + adjustment = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (canvas)); + ry -= gtk_adjustment_get_value (adjustment); + + gtk_window_move ( + GTK_WINDOW (arrow_down), + rx + x - ARROW_PTR, + ry - ARROW_DOWN_HEIGHT); + gtk_widget_show_all (arrow_down); + + gtk_window_move ( + GTK_WINDOW (arrow_up), + rx + x - ARROW_PTR, + ry + ethi->height); + gtk_widget_show_all (arrow_up); +} + +static void +ethi_add_destroy_marker (ETableHeaderItem *ethi) +{ + gdouble x1; + + if (ethi->remove_item) + g_object_run_dispose (G_OBJECT (ethi->remove_item)); + + x1 = (gdouble) e_table_header_col_diff (ethi->eth, 0, ethi->drag_col); + if (ethi->drag_col > 0) + x1 += ethi->group_indent_width; + + ethi->remove_item = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (GNOME_CANVAS_ITEM (ethi)->canvas->root), + gnome_canvas_rect_get_type (), + "x1", x1 + 1, + "y1", (gdouble) 1, + "x2", (gdouble) x1 + e_table_header_col_diff ( + ethi->eth, ethi->drag_col, ethi->drag_col + 1) - 2, + + "y2", (gdouble) ethi->height - 2, + "fill_color_rgba", 0xFF000080, + NULL); +} + +static void +ethi_remove_destroy_marker (ETableHeaderItem *ethi) +{ + if (!ethi->remove_item) + return; + + g_object_run_dispose (G_OBJECT (ethi->remove_item)); + ethi->remove_item = NULL; +} + +#if 0 +static gboolean +moved (ETableHeaderItem *ethi, + guint col, + guint model_col) +{ + if (col == -1) + return TRUE; + ecol = e_table_header_get_column (ethi->eth, col); + if (ecol->col_idx == model_col) + return FALSE; + if (col > 0) { + ecol = e_table_header_get_column (ethi->eth, col - 1); + if (ecol->col_idx == model_col) + return FALSE; + } + return TRUE; +} +#endif + +static void +do_drag_motion (ETableHeaderItem *ethi, + GdkDragContext *context, + gint x, + gint y, + guint time, + gboolean recreate) +{ + if ((x >= 0) && (x <= (ethi->width)) && + (y >= 0) && (y <= (ethi->height))) { + GdkDragAction suggested_action; + gint col; + d (g_print ("In header\n")); + + col = ethi_find_col_by_x_nearest (ethi, x); + suggested_action = gdk_drag_context_get_suggested_action (context); + + if (ethi->drag_col != -1 && (col == ethi->drag_col || + col == ethi->drag_col + 1)) { + if (ethi->drag_col != -1) + ethi_remove_destroy_marker (ethi); + + ethi_remove_drop_marker (ethi); + gdk_drag_status (context, suggested_action, time); + } + else if (col != -1) { + if (ethi->drag_col != -1) + ethi_remove_destroy_marker (ethi); + + ethi_add_drop_marker (ethi, col, recreate); + gdk_drag_status (context, suggested_action, time); + } else { + ethi_remove_drop_marker (ethi); + if (ethi->drag_col != -1) + ethi_add_destroy_marker (ethi); + } + } else { + ethi_remove_drop_marker (ethi); + if (ethi->drag_col != -1) + ethi_add_destroy_marker (ethi); + } +} + +static gboolean +scroll_timeout (gpointer data) +{ + ETableHeaderItem *ethi = data; + gint dx = 0; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + gdouble hadjustment_value; + gdouble vadjustment_value; + gdouble page_size; + gdouble lower; + gdouble upper; + gdouble value; + + if (ethi->scroll_direction & ET_SCROLL_RIGHT) + dx += 20; + if (ethi->scroll_direction & ET_SCROLL_LEFT) + dx -= 20; + + scrollable = GTK_SCROLLABLE (GNOME_CANVAS_ITEM (ethi)->canvas); + + adjustment = gtk_scrollable_get_hadjustment (scrollable); + hadjustment_value = gtk_adjustment_get_value (adjustment); + + adjustment = gtk_scrollable_get_vadjustment (scrollable); + vadjustment_value = gtk_adjustment_get_value (adjustment); + + value = hadjustment_value; + + adjustment = gtk_scrollable_get_hadjustment (scrollable); + page_size = gtk_adjustment_get_page_size (adjustment); + lower = gtk_adjustment_get_lower (adjustment); + upper = gtk_adjustment_get_upper (adjustment); + + gtk_adjustment_set_value ( + adjustment, CLAMP ( + hadjustment_value + dx, lower, upper - page_size)); + + hadjustment_value = gtk_adjustment_get_value (adjustment); + + if (hadjustment_value != value) + do_drag_motion ( + ethi, + ethi->last_drop_context, + ethi->last_drop_x + hadjustment_value, + ethi->last_drop_y + vadjustment_value, + ethi->last_drop_time, + TRUE); + + return TRUE; +} + +static void +scroll_on (ETableHeaderItem *ethi, + guint scroll_direction) +{ + if (ethi->scroll_idle_id == 0 || scroll_direction != ethi->scroll_direction) { + if (ethi->scroll_idle_id != 0) + g_source_remove (ethi->scroll_idle_id); + ethi->scroll_direction = scroll_direction; + ethi->scroll_idle_id = g_timeout_add (100, scroll_timeout, ethi); + } +} + +static void +scroll_off (ETableHeaderItem *ethi) +{ + if (ethi->scroll_idle_id) { + g_source_remove (ethi->scroll_idle_id); + ethi->scroll_idle_id = 0; + } +} + +static void +context_destroyed (gpointer data) +{ + ETableHeaderItem *ethi = data; + + ethi->last_drop_x = 0; + ethi->last_drop_y = 0; + ethi->last_drop_time = 0; + ethi->last_drop_context = NULL; + scroll_off (ethi); + + g_object_unref (ethi); +} + +static void +context_connect (ETableHeaderItem *ethi, + GdkDragContext *context) +{ + if (g_dataset_get_data (context, "e-table-header-item") == NULL) + g_dataset_set_data_full ( + context, "e-table-header-item", + g_object_ref (ethi), context_destroyed); +} + +static gboolean +ethi_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETableHeaderItem *ethi) +{ + GtkAllocation allocation; + GtkAdjustment *adjustment; + GList *targets; + gdouble hadjustment_value; + gdouble vadjustment_value; + gchar *droptype, *headertype; + guint direction = 0; + + gdk_drag_status (context, 0, time); + + targets = gdk_drag_context_list_targets (context); + droptype = gdk_atom_name (GDK_POINTER_TO_ATOM (targets->data)); + headertype = g_strdup_printf ( + "%s-%s", TARGET_ETABLE_COL_TYPE, ethi->dnd_code); + + if (strcmp (droptype, headertype) != 0) { + g_free (headertype); + return FALSE; + } + + g_free (headertype); + + gtk_widget_get_allocation (widget, &allocation); + + if (x < 20) + direction |= ET_SCROLL_LEFT; + if (x > allocation.width - 20) + direction |= ET_SCROLL_RIGHT; + + ethi->last_drop_x = x; + ethi->last_drop_y = y; + ethi->last_drop_time = time; + ethi->last_drop_context = context; + context_connect (ethi, context); + + adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (widget)); + hadjustment_value = gtk_adjustment_get_value (adjustment); + + adjustment = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (widget)); + vadjustment_value = gtk_adjustment_get_value (adjustment); + + do_drag_motion ( + ethi, context, + x + hadjustment_value, + y + vadjustment_value, + time, FALSE); + + if (direction != 0) + scroll_on (ethi, direction); + else + scroll_off (ethi); + + return TRUE; +} + +static void +ethi_drag_end (GtkWidget *canvas, + GdkDragContext *context, + ETableHeaderItem *ethi) +{ + ethi_remove_drop_marker (ethi); + ethi_remove_destroy_marker (ethi); + ethi->drag_col = -1; + scroll_off (ethi); +} + +static void +ethi_drag_data_received (GtkWidget *canvas, + GdkDragContext *drag_context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time, + ETableHeaderItem *ethi) +{ + const guchar *data; + gint found = FALSE; + gint count; + gint column; + gint drop_col; + gint i; + + data = gtk_selection_data_get_data (selection_data); + + if (data != NULL) { + count = e_table_header_count (ethi->eth); + column = atoi ((gchar *) data); + drop_col = ethi->drop_col; + ethi->drop_col = -1; + + if (column >= 0) { + for (i = 0; i < count; i++) { + ETableCol *ecol = e_table_header_get_column (ethi->eth, i); + if (ecol->col_idx == column) { + e_table_header_move (ethi->eth, i, drop_col); + found = TRUE; + break; + } + } + if (!found) { + count = e_table_header_count (ethi->full_header); + for (i = 0; i < count; i++) { + ETableCol *ecol; + + ecol = e_table_header_get_column ( + ethi->full_header, i); + + if (ecol->col_idx == column) { + e_table_header_add_column ( + ethi->eth, ecol, + drop_col); + break; + } + } + } + } + } + ethi_remove_drop_marker (ethi); + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ethi)); +} + +static void +ethi_drag_data_get (GtkWidget *canvas, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time, + ETableHeaderItem *ethi) +{ + if (ethi->drag_col != -1) { + ETableCol *ecol = e_table_header_get_column (ethi->eth, ethi->drag_col); + + gchar *string = g_strdup_printf ("%d", ecol->col_idx); + gtk_selection_data_set ( + selection_data, + GDK_SELECTION_TYPE_STRING, + sizeof (string[0]), + (guchar *) string, + strlen (string)); + g_free (string); + } +} + +static gboolean +ethi_drag_drop (GtkWidget *canvas, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETableHeaderItem *ethi) +{ + gboolean successful = FALSE; + + if ((x >= 0) && (x <= (ethi->width)) && + (y >= 0) && (y <= (ethi->height))) { + gint col; + + col = ethi_find_col_by_x_nearest (ethi, x); + + ethi_add_drop_marker (ethi, col, FALSE); + + ethi->drop_col = col; + + if (col != -1) { + gchar *target = g_strdup_printf ( + "%s-%s", TARGET_ETABLE_COL_TYPE, ethi->dnd_code); + d (g_print ("ethi - %s\n", target)); + gtk_drag_get_data ( + canvas, context, + gdk_atom_intern (target, FALSE), + time); + g_free (target); + } + } + gtk_drag_finish (context, successful, successful, time); + scroll_off (ethi); + return successful; +} + +static void +ethi_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time, + ETableHeaderItem *ethi) +{ + ethi_remove_drop_marker (ethi); + if (ethi->drag_col != -1) + ethi_add_destroy_marker (ethi); +} + +static void +ethi_realize (GnomeCanvasItem *item) +{ + ETableHeaderItem *ethi = E_TABLE_HEADER_ITEM (item); + GtkStyle *style; + GtkTargetEntry ethi_drop_types[] = { + { (gchar *) TARGET_ETABLE_COL_TYPE, 0, TARGET_ETABLE_COL_HEADER }, + }; + + if (GNOME_CANVAS_ITEM_CLASS (ethi_parent_class)-> realize) + (*GNOME_CANVAS_ITEM_CLASS (ethi_parent_class)->realize)(item); + + style = gtk_widget_get_style (GTK_WIDGET (item->canvas)); + + if (!ethi->font_desc) + ethi_font_set (ethi, style->font_desc); + + /* + * Now, configure DnD + */ + ethi_drop_types[0].target = g_strdup_printf ( + "%s-%s", ethi_drop_types[0].target, ethi->dnd_code); + gtk_drag_dest_set ( + GTK_WIDGET (item->canvas), 0, ethi_drop_types, + G_N_ELEMENTS (ethi_drop_types), GDK_ACTION_MOVE); + g_free ((gpointer) ethi_drop_types[0].target); + + /* Drop signals */ + ethi->drag_motion_id = g_signal_connect ( + item->canvas, "drag_motion", + G_CALLBACK (ethi_drag_motion), ethi); + ethi->drag_leave_id = g_signal_connect ( + item->canvas, "drag_leave", + G_CALLBACK (ethi_drag_leave), ethi); + ethi->drag_drop_id = g_signal_connect ( + item->canvas, "drag_drop", + G_CALLBACK (ethi_drag_drop), ethi); + ethi->drag_data_received_id = g_signal_connect ( + item->canvas, "drag_data_received", + G_CALLBACK (ethi_drag_data_received), ethi); + + /* Drag signals */ + ethi->drag_end_id = g_signal_connect ( + item->canvas, "drag_end", + G_CALLBACK (ethi_drag_end), ethi); + ethi->drag_data_get_id = g_signal_connect ( + item->canvas, "drag_data_get", + G_CALLBACK (ethi_drag_data_get), ethi); + +} + +static void +ethi_unrealize (GnomeCanvasItem *item) +{ + ETableHeaderItem *ethi = E_TABLE_HEADER_ITEM (item); + + if (ethi->font_desc != NULL) { + pango_font_description_free (ethi->font_desc); + ethi->font_desc = NULL; + } + + g_signal_handler_disconnect (item->canvas, ethi->drag_motion_id); + g_signal_handler_disconnect (item->canvas, ethi->drag_leave_id); + g_signal_handler_disconnect (item->canvas, ethi->drag_drop_id); + g_signal_handler_disconnect (item->canvas, ethi->drag_data_received_id); + + g_signal_handler_disconnect (item->canvas, ethi->drag_end_id); + g_signal_handler_disconnect (item->canvas, ethi->drag_data_get_id); + + gtk_drag_dest_unset (GTK_WIDGET (item->canvas)); + + if (GNOME_CANVAS_ITEM_CLASS (ethi_parent_class)->unrealize) + (*GNOME_CANVAS_ITEM_CLASS (ethi_parent_class)->unrealize)(item); +} + +static void +ethi_draw (GnomeCanvasItem *item, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height) +{ + ETableHeaderItem *ethi = E_TABLE_HEADER_ITEM (item); + GnomeCanvas *canvas = item->canvas; + const gint cols = e_table_header_count (ethi->eth); + gint x1, x2; + gint col; + GHashTable *arrows = g_hash_table_new (NULL, NULL); + GtkStyleContext *context; + + if (ethi->sort_info) { + gint length; + gint i; + + length = e_table_sort_info_grouping_get_count (ethi->sort_info); + for (i = 0; i < length; i++) { + ETableSortColumn column; + + column = e_table_sort_info_grouping_get_nth ( + ethi->sort_info, i); + + g_hash_table_insert ( + arrows, + GINT_TO_POINTER ((gint) column.column), + GINT_TO_POINTER ( + column.ascending ? + E_TABLE_COL_ARROW_DOWN : + E_TABLE_COL_ARROW_UP)); + } + + length = e_table_sort_info_sorting_get_count (ethi->sort_info); + for (i = 0; i < length; i++) { + ETableSortColumn column; + + column = e_table_sort_info_sorting_get_nth ( + ethi->sort_info, i); + + g_hash_table_insert ( + arrows, + GINT_TO_POINTER ((gint) column.column), + GINT_TO_POINTER ( + column.ascending ? + E_TABLE_COL_ARROW_DOWN : + E_TABLE_COL_ARROW_UP)); + } + } + + ethi->width = e_table_header_total_width (ethi->eth) + ethi->group_indent_width; + x1 = x2 = 0; + x2 += ethi->group_indent_width; + + context = gtk_widget_get_style_context (GTK_WIDGET (canvas)); + + for (col = 0; col < cols; col++, x1 = x2) { + ETableCol *ecol = e_table_header_get_column (ethi->eth, col); + gint col_width; + GtkRegionFlags flags = 0; + + col_width = ecol->width; + + x2 += col_width; + + if (x1 > (x + width)) + break; + + if (x2 < x) + continue; + + if (x2 <= x1) + continue; + + if (((col + 1) % 2) == 0) + flags |= GTK_REGION_EVEN; + else + flags |= GTK_REGION_ODD; + + if (col == 0) + flags |= GTK_REGION_FIRST; + + if (col + 1 == cols) + flags |= GTK_REGION_LAST; + + gtk_style_context_save (context); + gtk_style_context_add_region ( + context, GTK_STYLE_REGION_COLUMN_HEADER, flags); + + e_table_header_draw_button ( + cr, ecol, GTK_WIDGET (canvas), + x1 - x, -y, width, height, + x2 - x1, ethi->height, + (ETableColArrow) g_hash_table_lookup ( + arrows, GINT_TO_POINTER (ecol->col_idx))); + + gtk_style_context_restore (context); + } + + g_hash_table_destroy (arrows); +} + +static GnomeCanvasItem * +ethi_point (GnomeCanvasItem *item, + gdouble x, + gdouble y, + gint cx, + gint cy) +{ + return item; +} + +/* + * is_pointer_on_division: + * + * Returns whether @pos is a column header division; If @the_total is not NULL, + * then the actual position is returned here. If @return_ecol is not NULL, + * then the ETableCol that actually contains this point is returned here + */ +static gboolean +is_pointer_on_division (ETableHeaderItem *ethi, + gint pos, + gint *the_total, + gint *return_col) +{ + const gint cols = e_table_header_count (ethi->eth); + gint col, total; + + total = 0; + for (col = 0; col < cols; col++) { + ETableCol *ecol = e_table_header_get_column (ethi->eth, col); + + if (col == 0) + total += ethi->group_indent_width; + + total += ecol->width; + + if ((total - TOLERANCE < pos) && (pos < total + TOLERANCE)) { + if (return_col) + *return_col = col; + if (the_total) + *the_total = total; + + return TRUE; + } + if (return_col) + *return_col = col; + + if (total > pos + TOLERANCE) + return FALSE; + } + + return FALSE; +} + +#define convert(c,sx,sy,x,y) gnome_canvas_w2c (c,sx,sy,x,y) + +static void +set_cursor (ETableHeaderItem *ethi, + gint pos) +{ + GnomeCanvas *canvas; + GdkWindow *window; + gboolean resizable = FALSE; + gint col; + + canvas = GNOME_CANVAS_ITEM (ethi)->canvas; + window = gtk_widget_get_window (GTK_WIDGET (canvas)); + + /* We might be invoked before we are realized */ + if (window == NULL) + return; + + if (is_pointer_on_division (ethi, pos, NULL, &col)) { + gint last_col = ethi->eth->col_count - 1; + ETableCol *ecol = e_table_header_get_column (ethi->eth, col); + + /* Last column is not resizable */ + if (ecol->resizable && col != last_col) { + gint c = col + 1; + + /* Column is not resizable if all columns after it + * are also not resizable */ + for (; c <= last_col; c++) { + ETableCol *ecol2; + + ecol2 = e_table_header_get_column (ethi->eth, c); + if (ecol2->resizable) { + resizable = TRUE; + break; + } + } + } + } + + if (resizable) + gdk_window_set_cursor (window, ethi->resize_cursor); + else + gdk_window_set_cursor (window, NULL); +} + +static void +ethi_end_resize (ETableHeaderItem *ethi) +{ + ethi->resize_col = -1; + ethi->resize_guide = GINT_TO_POINTER (0); + + if (ethi->table) + e_table_thaw_state_change (ethi->table); + else if (ethi->tree) + e_tree_thaw_state_change (ethi->tree); + + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ethi)); +} + +static gboolean +ethi_maybe_start_drag (ETableHeaderItem *ethi, + GdkEventMotion *event) +{ + if (!ethi->maybe_drag) + return FALSE; + + if (ethi->eth->col_count < 2) { + ethi->maybe_drag = FALSE; + return FALSE; + } + + if (MAX (abs (ethi->click_x - event->x), + abs (ethi->click_y - event->y)) <= 3) + return FALSE; + + return TRUE; +} + +static void +ethi_start_drag (ETableHeaderItem *ethi, + GdkEvent *event) +{ + GtkWidget *widget = GTK_WIDGET (GNOME_CANVAS_ITEM (ethi)->canvas); + GtkTargetList *list; + GdkDragContext *context; + ETableCol *ecol; + gint col_width; + cairo_surface_t *s; + cairo_t *cr; + + gint group_indent = 0; + GHashTable *arrows = g_hash_table_new (NULL, NULL); + + GtkTargetEntry ethi_drag_types[] = { + { (gchar *) TARGET_ETABLE_COL_TYPE, 0, TARGET_ETABLE_COL_HEADER }, + }; + + widget = GTK_WIDGET (GNOME_CANVAS_ITEM (ethi)->canvas); + ethi->drag_col = ethi_find_col_by_x (ethi, event->motion.x); + + if (ethi->drag_col == -1) + return; + + if (ethi->sort_info) { + gint length = e_table_sort_info_grouping_get_count (ethi->sort_info); + gint i; + for (i = 0; i < length; i++) { + ETableSortColumn column = + e_table_sort_info_grouping_get_nth ( + ethi->sort_info, i); + group_indent++; + g_hash_table_insert ( + arrows, + GINT_TO_POINTER ((gint) column.column), + GINT_TO_POINTER ( + column.ascending ? + E_TABLE_COL_ARROW_DOWN : + E_TABLE_COL_ARROW_UP)); + } + length = e_table_sort_info_sorting_get_count (ethi->sort_info); + for (i = 0; i < length; i++) { + ETableSortColumn column = + e_table_sort_info_sorting_get_nth ( + ethi->sort_info, i); + + g_hash_table_insert ( + arrows, + GINT_TO_POINTER ((gint) column.column), + GINT_TO_POINTER ( + column.ascending ? + E_TABLE_COL_ARROW_DOWN : + E_TABLE_COL_ARROW_UP)); + } + } + + ethi_drag_types[0].target = g_strdup_printf ( + "%s-%s", ethi_drag_types[0].target, ethi->dnd_code); + list = gtk_target_list_new ( + ethi_drag_types, G_N_ELEMENTS (ethi_drag_types)); + context = gtk_drag_begin (widget, list, GDK_ACTION_MOVE, 1, event); + g_free ((gpointer) ethi_drag_types[0].target); + + ecol = e_table_header_get_column (ethi->eth, ethi->drag_col); + col_width = ecol->width; + s = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, col_width, ethi->height); + cr = cairo_create (s); + + e_table_header_draw_button ( + cr, ecol, + widget, 0, 0, + col_width, ethi->height, + col_width, ethi->height, + (ETableColArrow) g_hash_table_lookup ( + arrows, GINT_TO_POINTER (ecol->col_idx))); + gtk_drag_set_icon_surface (context, s); + cairo_surface_destroy (s); + + ethi->maybe_drag = FALSE; + g_hash_table_destroy (arrows); +} + +typedef struct { + ETableHeaderItem *ethi; + gint col; +} EthiHeaderInfo; + +static void +ethi_popup_sort_ascending (GtkWidget *widget, + EthiHeaderInfo *info) +{ + ETableCol *col; + gint model_col = -1; + gint length; + gint i; + gint found = FALSE; + ETableHeaderItem *ethi = info->ethi; + + col = e_table_header_get_column (ethi->eth, info->col); + if (col->sortable) + model_col = col->col_idx; + + length = e_table_sort_info_grouping_get_count (ethi->sort_info); + for (i = 0; i < length; i++) { + ETableSortColumn column = e_table_sort_info_grouping_get_nth ( + ethi->sort_info, i); + + if (model_col == column.column) { + column.ascending = 1; + e_table_sort_info_grouping_set_nth ( + ethi->sort_info, i, column); + found = 1; + break; + } + } + if (!found) { + length = e_table_sort_info_sorting_get_count ( + ethi->sort_info); + for (i = 0; i < length; i++) { + ETableSortColumn column = + e_table_sort_info_sorting_get_nth ( + ethi->sort_info, i); + if (model_col == column.column || model_col == -1) { + column.ascending = 1; + e_table_sort_info_sorting_set_nth ( + ethi->sort_info, i, column); + found = 1; + if (model_col != -1) + break; + } + } + } + if (!found) { + ETableSortColumn column; + column.column = model_col; + column.ascending = 1; + length = e_table_sort_info_sorting_get_count (ethi->sort_info); + if (length == 0) + length++; + e_table_sort_info_sorting_set_nth (ethi->sort_info, length - 1, column); + } +} + +static void +ethi_popup_sort_descending (GtkWidget *widget, + EthiHeaderInfo *info) +{ + ETableCol *col; + gint model_col=-1; + gint length; + gint i; + gint found = FALSE; + ETableHeaderItem *ethi = info->ethi; + + col = e_table_header_get_column (ethi->eth, info->col); + if (col->sortable) + model_col = col->col_idx; + + length = e_table_sort_info_grouping_get_count (ethi->sort_info); + for (i = 0; i < length; i++) { + ETableSortColumn column = e_table_sort_info_grouping_get_nth ( + ethi->sort_info, i); + if (model_col == column.column) { + column.ascending = 0; + e_table_sort_info_grouping_set_nth ( + ethi->sort_info, i, column); + found = 1; + break; + } + } + if (!found) { + length = e_table_sort_info_sorting_get_count (ethi->sort_info); + for (i = 0; i < length; i++) { + ETableSortColumn column = + e_table_sort_info_sorting_get_nth ( + ethi->sort_info, i); + + if (model_col == column.column || model_col == -1) { + column.ascending = 0; + e_table_sort_info_sorting_set_nth ( + ethi->sort_info, i, column); + found = 1; + if (model_col != -1) + break; + } + } + } + if (!found) { + ETableSortColumn column; + column.column = model_col; + column.ascending = 0; + length = e_table_sort_info_sorting_get_count (ethi->sort_info); + if (length == 0) + length++; + e_table_sort_info_sorting_set_nth ( + ethi->sort_info, length - 1, column); + } +} + +static void +ethi_popup_unsort (GtkWidget *widget, + EthiHeaderInfo *info) +{ + ETableHeaderItem *ethi = info->ethi; + + e_table_sort_info_grouping_truncate (ethi->sort_info, 0); + e_table_sort_info_sorting_truncate (ethi->sort_info, 0); +} + +static void +ethi_popup_group_field (GtkWidget *widget, + EthiHeaderInfo *info) +{ + ETableCol *col; + gint model_col; + ETableHeaderItem *ethi = info->ethi; + ETableSortColumn column; + + col = e_table_header_get_column (ethi->eth, info->col); + model_col = col->col_idx; + + column.column = model_col; + column.ascending = 1; + e_table_sort_info_grouping_set_nth (ethi->sort_info, 0, column); + e_table_sort_info_grouping_truncate (ethi->sort_info, 1); +} + +static void +ethi_popup_group_box (GtkWidget *widget, + EthiHeaderInfo *info) +{ +} + +static void +ethi_popup_remove_column (GtkWidget *widget, + EthiHeaderInfo *info) +{ + e_table_header_remove (info->ethi->eth, info->col); +} + +static void +ethi_popup_field_chooser (GtkWidget *widget, + EthiHeaderInfo *info) +{ + GtkWidget *etfcd = info->ethi->etfcd.widget; + + if (etfcd) { + gtk_window_present (GTK_WINDOW (etfcd)); + + return; + } + + info->ethi->etfcd.widget = e_table_field_chooser_dialog_new (); + etfcd = info->ethi->etfcd.widget; + + g_object_add_weak_pointer (G_OBJECT (etfcd), &info->ethi->etfcd.pointer); + + g_object_set ( + info->ethi->etfcd.widget, + "full_header", info->ethi->full_header, + "header", info->ethi->eth, + "dnd_code", info->ethi->dnd_code, + NULL); + + gtk_widget_show (etfcd); +} + +static void +ethi_popup_alignment (GtkWidget *widget, + EthiHeaderInfo *info) +{ +} + +static void +ethi_popup_best_fit (GtkWidget *widget, + EthiHeaderInfo *info) +{ + ETableHeaderItem *ethi = info->ethi; + gint width; + + g_signal_emit_by_name ( + ethi->eth, + "request_width", + info->col, &width); + /* Add 10 to stop it from "..."ing */ + e_table_header_set_size (ethi->eth, info->col, width + 10); + + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ethi)); + +} + +static void +ethi_popup_format_columns (GtkWidget *widget, + EthiHeaderInfo *info) +{ +} + +static void +config_destroyed (gpointer data, + GObject *where_object_was) +{ + ETableHeaderItem *ethi = data; + ethi->config = NULL; +} + +static void +apply_changes (ETableConfig *config, + ETableHeaderItem *ethi) +{ + gchar *state = e_table_state_save_to_string (config->state); + + if (ethi->table) + e_table_set_state (ethi->table, state); + if (ethi->tree) + e_tree_set_state (ethi->tree, state); + g_free (state); + + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (config->dialog_toplevel), + GTK_RESPONSE_APPLY, FALSE); +} + +static void +ethi_popup_customize_view (GtkWidget *widget, + EthiHeaderInfo *info) +{ + ETableHeaderItem *ethi = info->ethi; + ETableState *state; + ETableSpecification *spec; + + if (ethi->config) + e_table_config_raise (E_TABLE_CONFIG (ethi->config)); + else { + if (ethi->table) { + state = e_table_get_state_object (ethi->table); + spec = ethi->table->spec; + } else if (ethi->tree) { + state = e_tree_get_state_object (ethi->tree); + spec = e_tree_get_spec (ethi->tree); + } else + return; + + ethi->config = e_table_config_new ( + _("Customize Current View"), + spec, state, GTK_WINDOW (gtk_widget_get_toplevel (widget))); + g_object_weak_ref ( + G_OBJECT (ethi->config), + config_destroyed, ethi); + g_signal_connect ( + ethi->config, "changed", + G_CALLBACK (apply_changes), ethi); + } +} + +static void +free_popup_info (GtkWidget *w, + EthiHeaderInfo *info) +{ + g_free (info); +} + +/* Bit 1 is always disabled. */ +/* Bit 2 is disabled if not "sortable". */ +/* Bit 4 is disabled if we don't have a pointer to our table object. */ +static EPopupMenu ethi_context_menu[] = { + E_POPUP_ITEM ( + N_("Sort _Ascending"), + G_CALLBACK (ethi_popup_sort_ascending), 2), + E_POPUP_ITEM ( + N_("Sort _Descending"), + G_CALLBACK (ethi_popup_sort_descending), 2), + E_POPUP_ITEM ( + N_("_Unsort"), G_CALLBACK (ethi_popup_unsort), 0), + E_POPUP_SEPARATOR, + E_POPUP_ITEM ( + N_("Group By This _Field"), + G_CALLBACK (ethi_popup_group_field), 16), + E_POPUP_ITEM ( + N_("Group By _Box"), + G_CALLBACK (ethi_popup_group_box), 128), + E_POPUP_SEPARATOR, + E_POPUP_ITEM ( + N_("Remove This _Column"), + G_CALLBACK (ethi_popup_remove_column), 8), + E_POPUP_ITEM ( + N_("Add a C_olumn..."), + G_CALLBACK (ethi_popup_field_chooser), 0), + E_POPUP_SEPARATOR, + E_POPUP_ITEM ( + N_("A_lignment"), + G_CALLBACK (ethi_popup_alignment), 128), + E_POPUP_ITEM ( + N_("B_est Fit"), + G_CALLBACK (ethi_popup_best_fit), 2), + E_POPUP_ITEM ( + N_("Format Column_s..."), + G_CALLBACK (ethi_popup_format_columns), 128), + E_POPUP_SEPARATOR, + E_POPUP_ITEM ( + N_("Custo_mize Current View..."), + G_CALLBACK (ethi_popup_customize_view), 4), + E_POPUP_TERMINATOR +}; + +static void +sort_by_id (GtkWidget *menu_item, + ETableHeaderItem *ethi) +{ + ETableCol *ecol; + gboolean clearfirst; + gint col; + + col = GPOINTER_TO_INT (g_object_get_data ( + G_OBJECT (menu_item), "col-number")); + ecol = e_table_header_get_column (ethi->full_header, col); + clearfirst = e_table_sort_info_sorting_get_count (ethi->sort_info) > 1; + + if (!clearfirst && ecol && + e_table_sort_info_sorting_get_count (ethi->sort_info) == 1) { + ETableSortColumn column; + + column = e_table_sort_info_sorting_get_nth (ethi->sort_info, 0); + clearfirst = ecol->sortable && ecol->col_idx != column.column; + } + + if (clearfirst) + e_table_sort_info_sorting_truncate (ethi->sort_info, 0); + + ethi_change_sort_state (ethi, ecol); +} + +static void +popup_custom (GtkWidget *menu_item, + EthiHeaderInfo *info) +{ + ethi_popup_customize_view (menu_item, info); +} + +static void +ethi_header_context_menu (ETableHeaderItem *ethi, + GdkEvent *button_event) +{ + EthiHeaderInfo *info = g_new (EthiHeaderInfo, 1); + GtkMenu *popup; + gint ncol, sort_count, sort_col; + GtkWidget *menu_item, *sub_menu; + ETableSortColumn column; + gboolean ascending = TRUE; + gdouble event_x_win = 0; + gdouble event_y_win = 0; + guint event_button = 0; + guint32 event_time; + + d (g_print ("ethi_header_context_menu: \n")); + + gdk_event_get_button (button_event, &event_button); + gdk_event_get_coords (button_event, &event_x_win, &event_y_win); + event_time = gdk_event_get_time (button_event); + + info->ethi = ethi; + info->col = ethi_find_col_by_x (ethi, event_x_win); + + popup = e_popup_menu_create_with_domain ( + ethi_context_menu, + 1 + + ((ethi->table || ethi->tree) ? 0 : 4) + + ((e_table_header_count (ethi->eth) > 1) ? 0 : 8), + ((e_table_sort_info_get_can_group (ethi->sort_info)) ? 0 : 16) + + 128, info, GETTEXT_PACKAGE); + + menu_item = gtk_menu_item_new_with_mnemonic (_("_Sort By")); + gtk_widget_show (menu_item); + sub_menu = gtk_menu_new (); + gtk_widget_show (sub_menu); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu_item), sub_menu); + gtk_menu_shell_prepend (GTK_MENU_SHELL (popup), menu_item); + + sort_count = e_table_sort_info_sorting_get_count (ethi->sort_info); + + if (sort_count > 1 || sort_count < 1) + sort_col = -1; /* Custom sorting */ + else { + column = e_table_sort_info_sorting_get_nth (ethi->sort_info, 0); + sort_col = column.column; + ascending = column.ascending; + } + + /* Custom */ + menu_item = gtk_check_menu_item_new_with_mnemonic (_("_Custom")); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (sub_menu), menu_item); + if (sort_col == -1) + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menu_item), TRUE); + gtk_check_menu_item_set_draw_as_radio (GTK_CHECK_MENU_ITEM (menu_item), TRUE); + g_signal_connect ( + menu_item, "activate", + G_CALLBACK (popup_custom), info); + + /* Show a seperator */ + menu_item = gtk_separator_menu_item_new (); + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (sub_menu), menu_item); + /* Headers */ + for (ncol = 0; ncol < ethi->full_header->col_count; ncol++) + { + gchar *text = NULL; + + if (!ethi->full_header->columns[ncol]->sortable || + ethi->full_header->columns[ncol]->disabled) + continue; + + if (ncol == sort_col) { + text = g_strdup_printf ( + "%s (%s)", + ethi->full_header->columns[ncol]->text, + ascending ? _("Ascending"):_("Descending")); + menu_item = gtk_check_menu_item_new_with_label (text); + g_free (text); + } else + menu_item = gtk_check_menu_item_new_with_label ( + ethi->full_header->columns[ncol]->text); + + gtk_widget_show (menu_item); + gtk_menu_shell_prepend (GTK_MENU_SHELL (sub_menu), menu_item); + + if (ncol == sort_col) + gtk_check_menu_item_set_active ( + GTK_CHECK_MENU_ITEM (menu_item), TRUE); + gtk_check_menu_item_set_draw_as_radio ( + GTK_CHECK_MENU_ITEM (menu_item), TRUE); + g_object_set_data ( + G_OBJECT (menu_item), "col-number", + GINT_TO_POINTER (ncol)); + g_signal_connect ( + menu_item, "activate", + G_CALLBACK (sort_by_id), ethi); + } + + g_object_ref_sink (popup); + g_signal_connect ( + popup, "selection-done", + G_CALLBACK (free_popup_info), info); + + gtk_menu_popup ( + GTK_MENU (popup), + NULL, NULL, NULL, NULL, + event_button, event_time); +} + +static void +ethi_button_pressed (ETableHeaderItem *ethi, + GdkEvent *button_event) +{ + g_signal_emit (ethi, ethi_signals[BUTTON_PRESSED], 0, button_event); +} + +void +ethi_change_sort_state (ETableHeaderItem *ethi, + ETableCol *col) +{ + gint model_col = -1; + gint length; + gint i; + gboolean found = FALSE; + + if (col == NULL) + return; + + if (col->sortable) + model_col = col->col_idx; + + length = e_table_sort_info_grouping_get_count (ethi->sort_info); + for (i = 0; i < length; i++) { + ETableSortColumn column; + + column = e_table_sort_info_grouping_get_nth ( + ethi->sort_info, i); + + if (model_col == column.column || model_col == -1) { + gint ascending = column.ascending; + ascending = !ascending; + column.ascending = ascending; + e_table_sort_info_grouping_set_nth (ethi->sort_info, i, column); + found = TRUE; + if (model_col != -1) + break; + } + } + + if (!found) { + length = e_table_sort_info_sorting_get_count (ethi->sort_info); + for (i = 0; i < length; i++) { + ETableSortColumn column; + + column = e_table_sort_info_sorting_get_nth ( + ethi->sort_info, i); + + if (model_col == column.column || model_col == -1) { + gint ascending = column.ascending; + + if (ascending == 0 && model_col != -1) { + /* + * This means the user has clicked twice + * already, lets kill sorting of this column now. + */ + gint j; + + for (j = i + 1; j < length; j++) + e_table_sort_info_sorting_set_nth ( + ethi->sort_info, j - 1, + e_table_sort_info_sorting_get_nth ( + ethi->sort_info, j)); + + e_table_sort_info_sorting_truncate ( + ethi->sort_info, length - 1); + length--; + i--; + } else { + ascending = !ascending; + column.ascending = ascending; + e_table_sort_info_sorting_set_nth ( + ethi->sort_info, i, column); + } + found = TRUE; + if (model_col != -1) + break; + } + } + } + + if (!found && model_col != -1) { + ETableSortColumn column; + column.column = model_col; + column.ascending = 1; + e_table_sort_info_sorting_truncate (ethi->sort_info, 0); + e_table_sort_info_sorting_set_nth (ethi->sort_info, 0, column); + } +} + +/* + * Handles the events on the ETableHeaderItem, particularly it handles resizing + */ +static gint +ethi_event (GnomeCanvasItem *item, + GdkEvent *event) +{ + ETableHeaderItem *ethi = E_TABLE_HEADER_ITEM (item); + GnomeCanvas *canvas = item->canvas; + GdkWindow *window; + const gboolean resizing = ETHI_RESIZING (ethi); + gint x, y, start, col; + gint was_maybe_drag = 0; + GdkModifierType event_state = 0; + guint event_button = 0; + guint event_keyval = 0; + gdouble event_x_win = 0; + gdouble event_y_win = 0; + guint32 event_time; + + /* Don't fetch the device here. GnomeCanvas frequently emits + * synthesized events, and calling gdk_event_get_device() on them + * will trigger a runtime warning. Fetch the device where needed. */ + gdk_event_get_button (event, &event_button); + gdk_event_get_coords (event, &event_x_win, &event_y_win); + gdk_event_get_keyval (event, &event_keyval); + gdk_event_get_state (event, &event_state); + event_time = gdk_event_get_time (event); + + switch (event->type) { + case GDK_ENTER_NOTIFY: + convert (canvas, event_x_win, event_y_win, &x, &y); + set_cursor (ethi, x); + break; + + case GDK_LEAVE_NOTIFY: + window = gtk_widget_get_window (GTK_WIDGET (canvas)); + gdk_window_set_cursor (window, NULL); + break; + + case GDK_MOTION_NOTIFY: + + convert (canvas, event_x_win, event_y_win, &x, &y); + if (resizing) { + gint new_width; + + if (ethi->resize_guide == NULL) { + GdkDevice *event_device; + + /* Quick hack until I actually bind the views */ + ethi->resize_guide = GINT_TO_POINTER (1); + + event_device = gdk_event_get_device (event); + + gnome_canvas_item_grab ( + item, + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_RELEASE_MASK, + ethi->resize_cursor, + event_device, + event_time); + } + + new_width = x - ethi->resize_start_pos; + + e_table_header_set_size (ethi->eth, ethi->resize_col, new_width); + + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ethi)); + } else if (ethi_maybe_start_drag (ethi, &event->motion)) { + ethi_start_drag (ethi, event); + } else + set_cursor (ethi, x); + break; + + case GDK_BUTTON_PRESS: + if (event_button > 3) + return FALSE; + + convert (canvas, event_x_win, event_y_win, &x, &y); + + if (is_pointer_on_division (ethi, x, &start, &col) && + event_button == 1) { + ETableCol *ecol; + + /* + * Record the important bits. + * + * By setting resize_pos to a non -1 value, + * we know that we are being resized (used in the + * other event handlers). + */ + ecol = e_table_header_get_column (ethi->eth, col); + + if (!ecol->resizable) + break; + ethi->resize_col = col; + ethi->resize_start_pos = start - ecol->width; + ethi->resize_min_width = ecol->min_width; + + if (ethi->table) + e_table_freeze_state_change (ethi->table); + else if (ethi->tree) + e_tree_freeze_state_change (ethi->tree); + } else { + if (event_button == 1) { + ethi->click_x = event_x_win; + ethi->click_y = event_y_win; + ethi->maybe_drag = TRUE; + is_pointer_on_division (ethi, x, &start, &col); + ethi->selected_col = col; + if (gtk_widget_get_can_focus (GTK_WIDGET (item->canvas))) + e_canvas_item_grab_focus (item, TRUE); + } else if (event_button == 3) { + ethi_header_context_menu (ethi, event); + } else + ethi_button_pressed (ethi, event); + } + break; + + case GDK_2BUTTON_PRESS: + if (!resizing) + break; + + if (event_button != 1) + break; + else { + gint width = 0; + g_signal_emit_by_name ( + ethi->eth, + "request_width", + (gint) ethi->resize_col, &width); + /* Add 10 to stop it from "..."ing */ + e_table_header_set_size (ethi->eth, ethi->resize_col, width + 10); + + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (ethi)); + ethi->maybe_drag = FALSE; + } + break; + + case GDK_BUTTON_RELEASE: { + gboolean needs_ungrab = FALSE; + + was_maybe_drag = ethi->maybe_drag; + + ethi->maybe_drag = FALSE; + + if (ethi->resize_col != -1) { + needs_ungrab = (ethi->resize_guide != NULL); + ethi_end_resize (ethi); + } else if (was_maybe_drag && ethi->sort_info) { + ETableCol *ecol; + + col = ethi_find_col_by_x (ethi, event_x_win); + ecol = e_table_header_get_column (ethi->eth, col); + ethi_change_sort_state (ethi, ecol); + } + + if (needs_ungrab) + gnome_canvas_item_ungrab (item, event_time); + + break; + } + case GDK_KEY_PRESS: + if ((event_keyval == GDK_KEY_F10) && (event_state & GDK_SHIFT_MASK)) { + EthiHeaderInfo *info = g_new (EthiHeaderInfo, 1); + ETableCol *ecol; + GtkMenu *popup; + + info->ethi = ethi; + info->col = ethi->selected_col; + ecol = e_table_header_get_column (ethi->eth, info->col); + + popup = e_popup_menu_create_with_domain ( + ethi_context_menu, + 1 + + (ecol->sortable ? 0 : 2) + + ((ethi->table || ethi->tree) ? 0 : 4) + + ((e_table_header_count (ethi->eth) > 1) ? 0 : 8), + ((e_table_sort_info_get_can_group ( + ethi->sort_info)) ? 0 : 16) + + 128, info, GETTEXT_PACKAGE); + g_object_ref_sink (popup); + g_signal_connect ( + popup, "selection-done", + G_CALLBACK (free_popup_info), info); + gtk_menu_popup ( + GTK_MENU (popup), + NULL, NULL, NULL, NULL, + 0, GDK_CURRENT_TIME); + } else if (event_keyval == GDK_KEY_space) { + ETableCol *ecol; + + ecol = e_table_header_get_column (ethi->eth, ethi->selected_col); + ethi_change_sort_state (ethi, ecol); + } else if ((event_keyval == GDK_KEY_Right) || + (event_keyval == GDK_KEY_KP_Right)) { + ETableCol *ecol; + + if ((ethi->selected_col < 0) || + (ethi->selected_col >= ethi->eth->col_count - 1)) + ethi->selected_col = 0; + else + ethi->selected_col++; + ecol = e_table_header_get_column (ethi->eth, ethi->selected_col); + ethi_change_sort_state (ethi, ecol); + } else if ((event_keyval == GDK_KEY_Left) || + (event_keyval == GDK_KEY_KP_Left)) { + ETableCol *ecol; + + if ((ethi->selected_col <= 0) || + (ethi->selected_col >= ethi->eth->col_count)) + ethi->selected_col = ethi->eth->col_count - 1; + else + ethi->selected_col--; + ecol = e_table_header_get_column (ethi->eth, ethi->selected_col); + ethi_change_sort_state (ethi, ecol); + } + break; + + default: + return FALSE; + } + return TRUE; +} + +static void +ethi_class_init (ETableHeaderItemClass *class) +{ + GnomeCanvasItemClass *item_class = GNOME_CANVAS_ITEM_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = ethi_dispose; + object_class->set_property = ethi_set_property; + object_class->get_property = ethi_get_property; + + item_class->update = ethi_update; + item_class->realize = ethi_realize; + item_class->unrealize = ethi_unrealize; + item_class->draw = ethi_draw; + item_class->point = ethi_point; + item_class->event = ethi_event; + + g_object_class_install_property ( + object_class, + PROP_DND_CODE, + g_param_spec_string ( + "dnd_code", + "DnD code", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_FONT_DESC, + g_param_spec_boxed ( + "font-desc", + "Font Description", + NULL, + PANGO_TYPE_FONT_DESCRIPTION, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_FULL_HEADER, + g_param_spec_object ( + "full_header", + "Full Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_HEADER, + g_param_spec_object ( + "ETableHeader", + "Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_SORT_INFO, + g_param_spec_object ( + "sort_info", + "Sort Info", + NULL, + E_TYPE_TABLE_SORT_INFO, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE, + g_param_spec_object ( + "table", + "Table", + NULL, + E_TYPE_TABLE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TREE, + g_param_spec_object ( + "tree", + "Tree", + NULL, + E_TYPE_TREE, + G_PARAM_WRITABLE)); + + ethi_signals[BUTTON_PRESSED] = g_signal_new ( + "button_pressed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableHeaderItemClass, button_pressed), + NULL, NULL, + g_cclosure_marshal_VOID__BOXED, + G_TYPE_NONE, 1, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); +} + +static void +ethi_init (ETableHeaderItem *ethi) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (ethi); + + ethi->resize_cursor = gdk_cursor_new (GDK_SB_H_DOUBLE_ARROW); + + ethi->resize_col = -1; + + item->x1 = 0; + item->y1 = 0; + item->x2 = 0; + item->y2 = 0; + + ethi->drag_col = -1; + ethi->drag_mark = -1; + + ethi->sort_info = NULL; + + ethi->sort_info_changed_id = 0; + ethi->group_info_changed_id = 0; + + ethi->group_indent_width = 0; + ethi->table = NULL; + ethi->tree = NULL; + + ethi->selected_col = 0; +} + diff --git a/e-util/e-table-header-item.h b/e-util/e-table-header-item.h new file mode 100644 index 0000000000..1cd0c717ab --- /dev/null +++ b/e-util/e-table-header-item.h @@ -0,0 +1,148 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza (miguel@gnu.org) + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_HEADER_ITEM_H_ +#define _E_TABLE_HEADER_ITEM_H_ + +#include <libxml/tree.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-table-header.h> +#include <e-util/e-table-sort-info.h> +#include <e-util/e-table.h> +#include <e-util/e-tree.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_HEADER_ITEM \ + (e_table_header_item_get_type ()) +#define E_TABLE_HEADER_ITEM(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_HEADER_ITEM, ETableHeaderItem)) +#define E_TABLE_HEADER_ITEM_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_HEADER_ITEM, ETableHeaderItemClass)) +#define E_IS_TABLE_HEADER_ITEM(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_HEADER_ITEM)) +#define E_IS_TABLE_HEADER_ITEM_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_HEADER_ITEM)) +#define E_TABLE_HEADER_ITEM_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_HEADER_ITEM, ETableHeaderItemClass)) + +G_BEGIN_DECLS + +typedef struct _ETableHeaderItem ETableHeaderItem; +typedef struct _ETableHeaderItemClass ETableHeaderItemClass; + +struct _ETableHeaderItem { + GnomeCanvasItem parent; + ETableHeader *eth; + + GdkCursor *change_cursor; + GdkCursor *resize_cursor; + + gshort height, width; + PangoFontDescription *font_desc; + + /* + * Used during resizing; Could be shorts + */ + gint resize_col; + gint resize_start_pos; + gint resize_min_width; + + gpointer resize_guide; + + gint group_indent_width; + + /* + * Ids + */ + gint structure_change_id, dimension_change_id; + + /* + * For dragging columns + */ + guint maybe_drag : 1; + guint dnd_ready : 1; + gint click_x, click_y; + gint drag_col, drop_col, drag_mark; + guint drag_motion_id; + guint drag_end_id; + guint drag_leave_id; + guint drag_drop_id; + guint drag_data_received_id; + guint drag_data_get_id; + guint sort_info_changed_id, group_info_changed_id; + GnomeCanvasItem *remove_item; + + gchar *dnd_code; + + /* + * For column sorting info + */ + ETableSortInfo *sort_info; + + guint scroll_direction : 4; + gint last_drop_x; + gint last_drop_y; + gint last_drop_time; + GdkDragContext *last_drop_context; + gint scroll_idle_id; + + /* For adding fields. */ + ETableHeader *full_header; + ETable *table; + ETree *tree; + gpointer config; + + union { + GtkWidget *widget; + gpointer pointer; + } etfcd; + + /* For keyboard navigation*/ + gint selected_col; +}; + +struct _ETableHeaderItemClass { + GnomeCanvasItemClass parent_class; + + /* Signals */ + void (*button_pressed) (ETableHeaderItem *ethi, + GdkEvent *button_event); +}; + +GType e_table_header_item_get_type (void) G_GNUC_CONST; +void ethi_change_sort_state (ETableHeaderItem *ethi, + ETableCol *col); + +G_END_DECLS + +#endif /* _E_TABLE_HEADER_ITEM_H_ */ diff --git a/e-util/e-table-header-utils.c b/e-util/e-table-header-utils.c new file mode 100644 index 0000000000..d3ee1aca38 --- /dev/null +++ b/e-util/e-table-header-utils.c @@ -0,0 +1,282 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * Federico Mena-Quintero <federico@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-header-utils.h" + +#include <string.h> /* strlen() */ + +#include <gtk/gtk.h> + +#include "e-table-defines.h" +#include "e-unicode.h" + +static void +get_button_padding (GtkWidget *widget, + GtkBorder *padding) +{ + GtkStyleContext *context; + GtkStateFlags state_flags; + + context = gtk_widget_get_style_context (widget); + state_flags = gtk_widget_get_state_flags (widget); + + gtk_style_context_save (context); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_BUTTON); + gtk_style_context_get_padding (context, state_flags, padding); + + gtk_style_context_restore (context); +} + +/** + * e_table_header_compute_height: + * @ecol: Table column description. + * @widget: The widget from which to build the PangoLayout. + * + * Computes the minimum height required for a table header button. + * + * Return value: The height of the button, in pixels. + **/ +gdouble +e_table_header_compute_height (ETableCol *ecol, + GtkWidget *widget) +{ + gint height; + PangoLayout *layout; + GtkBorder padding; + + g_return_val_if_fail (ecol != NULL, -1); + g_return_val_if_fail (E_IS_TABLE_COL (ecol), -1); + g_return_val_if_fail (GTK_IS_WIDGET (widget), -1); + + get_button_padding (widget, &padding); + + layout = gtk_widget_create_pango_layout (widget, ecol->text); + + pango_layout_get_pixel_size (layout, NULL, &height); + + if (ecol->icon_name != NULL) { + g_return_val_if_fail (ecol->pixbuf != NULL, -1); + height = MAX (height, gdk_pixbuf_get_height (ecol->pixbuf)); + } + + height = MAX (height, MIN_ARROW_SIZE); + height += padding.top + padding.bottom + 2 * HEADER_PADDING; + + g_object_unref (layout); + + return height; +} + +gdouble +e_table_header_width_extras (GtkWidget *widget) +{ + GtkBorder padding; + + get_button_padding (widget, &padding); + return padding.left + padding.right + 2 * HEADER_PADDING; +} + +/** + * e_table_header_draw_button: + * @drawable: Destination drawable. + * @ecol: Table column for the header information. + * @widget: The table widget. + * @x: Leftmost coordinate of the button. + * @y: Topmost coordinate of the button. + * @width: Width of the region to draw. + * @height: Height of the region to draw. + * @button_width: Width for the complete button. + * @button_height: Height for the complete button. + * @arrow: Arrow type to use as a sort indicator. + * + * Draws a button suitable for a table header. + **/ +void +e_table_header_draw_button (cairo_t *cr, + ETableCol *ecol, + GtkWidget *widget, + gint x, + gint y, + gint width, + gint height, + gint button_width, + gint button_height, + ETableColArrow arrow) +{ + gint inner_x, inner_y; + gint inner_width, inner_height; + gint arrow_width = 0, arrow_height = 0; + PangoContext *pango_context; + PangoLayout *layout; + GtkStyleContext *context; + GtkBorder padding; + GtkStateFlags state_flags; + + g_return_if_fail (cr != NULL); + g_return_if_fail (ecol != NULL); + g_return_if_fail (E_IS_TABLE_COL (ecol)); + g_return_if_fail (widget != NULL); + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (button_width > 0 && button_height > 0); + + /* Button bevel */ + context = gtk_widget_get_style_context (widget); + state_flags = gtk_widget_get_state_flags (widget); + + gtk_style_context_save (context); + gtk_style_context_set_state (context, state_flags); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_BUTTON); + + gtk_style_context_get_padding (context, state_flags, &padding); + + gtk_render_background ( + context, cr, x, y, + button_width, button_height); + gtk_render_frame ( + context, cr, x, y, + button_width, button_height); + + /* Inside area */ + + inner_width = + button_width - + (padding.left + padding.right + 2 * HEADER_PADDING); + inner_height = + button_height - + (padding.top + padding.bottom + 2 * HEADER_PADDING); + + if (inner_width < 1 || inner_height < 1) { + return; /* nothing fits */ + } + + inner_x = x + padding.left + HEADER_PADDING; + inner_y = y + padding.top + HEADER_PADDING; + + /* Arrow space */ + + switch (arrow) { + case E_TABLE_COL_ARROW_NONE: + break; + + case E_TABLE_COL_ARROW_UP: + case E_TABLE_COL_ARROW_DOWN: + arrow_width = MIN (MIN_ARROW_SIZE, inner_width); + arrow_height = MIN (MIN_ARROW_SIZE, inner_height); + + if (ecol->icon_name == NULL) + inner_width -= arrow_width + HEADER_PADDING; + break; + default: + cairo_restore (cr); + g_return_if_reached (); + } + + if (inner_width < 1) { + gtk_style_context_restore (context); + return; /* nothing else fits */ + } + + pango_context = gtk_widget_create_pango_context (widget); + layout = pango_layout_new (pango_context); + g_object_unref (pango_context); + + pango_layout_set_text (layout, ecol->text, -1); + pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_END); + + /* Pixbuf or label */ + if (ecol->icon_name != NULL) { + gint pwidth, pheight; + gint clip_height; + gint xpos; + + g_return_if_fail (ecol->pixbuf != NULL); + + pwidth = gdk_pixbuf_get_width (ecol->pixbuf); + pheight = gdk_pixbuf_get_height (ecol->pixbuf); + + clip_height = MIN (pheight, inner_height); + + xpos = inner_x; + + if (inner_width - pwidth > 11) { + gint ypos; + + pango_layout_get_pixel_size (layout, &width, NULL); + + if (width < inner_width - (pwidth + 1)) { + xpos = inner_x + (inner_width - width - (pwidth + 1)) / 2; + } + + ypos = inner_y; + + pango_layout_set_width ( + layout, (inner_width - (xpos - inner_x)) * + PANGO_SCALE); + + gtk_render_layout ( + context, cr, xpos + pwidth + 1, + ypos, layout); + } + + gtk_render_icon ( + context, cr, ecol->pixbuf, xpos, + inner_y + (inner_height - clip_height) / 2); + + } else { + pango_layout_set_width (layout, inner_width * PANGO_SCALE); + + gtk_render_layout (context, cr, inner_x, inner_y, layout); + } + + switch (arrow) { + case E_TABLE_COL_ARROW_NONE: + break; + + case E_TABLE_COL_ARROW_UP: + case E_TABLE_COL_ARROW_DOWN: { + if (ecol->icon_name == NULL) + inner_width += arrow_width + HEADER_PADDING; + + gtk_render_arrow ( + context, cr, + (arrow == E_TABLE_COL_ARROW_UP) ? 0 : G_PI, + inner_x + inner_width - arrow_width, + inner_y + (inner_height - arrow_height) / 2, + MAX (arrow_width, arrow_height)); + + break; + } + + default: + cairo_restore (cr); + g_return_if_reached (); + } + + g_object_unref (layout); + gtk_style_context_restore (context); +} diff --git a/e-util/e-table-header-utils.h b/e-util/e-table-header-utils.h new file mode 100644 index 0000000000..3022681caa --- /dev/null +++ b/e-util/e-table-header-utils.h @@ -0,0 +1,53 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * Federico Mena-Quintero <federico@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_TABLE_HEADER_UTILS_H +#define E_TABLE_HEADER_UTILS_H + +#include <e-util/e-table-col.h> + +G_BEGIN_DECLS + +gdouble e_table_header_compute_height (ETableCol *ecol, + GtkWidget *widget); +gdouble e_table_header_width_extras (GtkWidget *widget); +void e_table_header_draw_button (cairo_t *cr, + ETableCol *ecol, + GtkWidget *widget, + gint x, + gint y, + gint width, + gint height, + gint button_width, + gint button_height, + ETableColArrow arrow); + +G_END_DECLS + +#endif /* E_TABLE_HEADER_UTILS_H */ diff --git a/e-util/e-table-header.c b/e-util/e-table-header.c new file mode 100644 index 0000000000..d06b26e147 --- /dev/null +++ b/e-util/e-table-header.c @@ -0,0 +1,1013 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include <gtk/gtk.h> + +#include "e-marshal.h" +#include "e-table-defines.h" +#include "e-table-header.h" + +enum { + PROP_0, + PROP_SORT_INFO, + PROP_WIDTH, + PROP_WIDTH_EXTRAS +}; + +enum { + STRUCTURE_CHANGE, + DIMENSION_CHANGE, + EXPANSION_CHANGE, + REQUEST_WIDTH, + LAST_SIGNAL +}; + +static void eth_set_size (ETableHeader *eth, gint idx, gint size); +static void eth_calc_widths (ETableHeader *eth); + +static guint eth_signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE (ETableHeader, e_table_header, G_TYPE_OBJECT) + +struct two_ints { + gint column; + gint width; +}; + +static void +eth_set_width (ETableHeader *eth, + gint width) +{ + eth->width = width; +} + +static void +dequeue (ETableHeader *eth, + gint *column, + gint *width) +{ + GSList *head; + struct two_ints *store; + head = eth->change_queue; + eth->change_queue = eth->change_queue->next; + if (!eth->change_queue) + eth->change_tail = NULL; + store = head->data; + g_slist_free_1 (head); + if (column) + *column = store->column; + if (width) + *width = store->width; + g_free (store); +} + +static gboolean +dequeue_idle (ETableHeader *eth) +{ + gint column, width; + + dequeue (eth, &column, &width); + while (eth->change_queue && ((struct two_ints *) + eth->change_queue->data)->column == column) + dequeue (eth, &column, &width); + + if (column == -1) + eth_set_width (eth, width); + else if (column < eth->col_count) + eth_set_size (eth, column, width); + if (eth->change_queue) + return TRUE; + else { + eth_calc_widths (eth); + eth->idle = 0; + return FALSE; + } +} + +static void +enqueue (ETableHeader *eth, + gint column, + gint width) +{ + struct two_ints *store; + store = g_new (struct two_ints, 1); + store->column = column; + store->width = width; + + eth->change_tail = g_slist_last (g_slist_append (eth->change_tail, store)); + if (!eth->change_queue) + eth->change_queue = eth->change_tail; + + if (!eth->idle) { + eth->idle = g_idle_add_full ( + G_PRIORITY_LOW, (GSourceFunc) + dequeue_idle, eth, NULL); + } +} + +void +e_table_header_set_size (ETableHeader *eth, + gint idx, + gint size) +{ + g_return_if_fail (eth != NULL); + g_return_if_fail (E_IS_TABLE_HEADER (eth)); + + enqueue (eth, idx, size); +} + +static void +eth_do_remove (ETableHeader *eth, + gint idx, + gboolean do_unref) +{ + if (do_unref) + g_object_unref (eth->columns[idx]); + + memmove ( + ð->columns[idx], ð->columns[idx + 1], + sizeof (ETableCol *) * (eth->col_count - idx - 1)); + eth->col_count--; +} + +static void +eth_finalize (GObject *object) +{ + ETableHeader *eth = E_TABLE_HEADER (object); + const gint cols = eth->col_count; + gint i; + + if (eth->sort_info) { + if (eth->sort_info_group_change_id) + g_signal_handler_disconnect ( + eth->sort_info, + eth->sort_info_group_change_id); + g_object_unref (eth->sort_info); + eth->sort_info = NULL; + } + + if (eth->idle) + g_source_remove (eth->idle); + eth->idle = 0; + + if (eth->change_queue) { + g_slist_foreach (eth->change_queue, (GFunc) g_free, NULL); + g_slist_free (eth->change_queue); + eth->change_queue = NULL; + } + + /* + * Destroy columns + */ + for (i = cols - 1; i >= 0; i--) { + eth_do_remove (eth, i, TRUE); + } + g_free (eth->columns); + + eth->col_count = 0; + eth->columns = NULL; + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_table_header_parent_class)->finalize (object); +} + +static void +eth_group_info_changed (ETableSortInfo *info, + ETableHeader *eth) +{ + enqueue (eth, -1, eth->nominal_width); +} + +static void +eth_set_property (GObject *object, + guint property_id, + const GValue *val, + GParamSpec *pspec) +{ + ETableHeader *eth = E_TABLE_HEADER (object); + + switch (property_id) { + case PROP_WIDTH: + eth->nominal_width = g_value_get_double (val); + enqueue (eth, -1, eth->nominal_width); + break; + case PROP_WIDTH_EXTRAS: + eth->width_extras = g_value_get_double (val); + enqueue (eth, -1, eth->nominal_width); + break; + case PROP_SORT_INFO: + if (eth->sort_info) { + if (eth->sort_info_group_change_id) + g_signal_handler_disconnect ( + eth->sort_info, + eth->sort_info_group_change_id); + g_object_unref (eth->sort_info); + } + eth->sort_info = E_TABLE_SORT_INFO (g_value_get_object (val)); + if (eth->sort_info) { + g_object_ref (eth->sort_info); + eth->sort_info_group_change_id = g_signal_connect ( + eth->sort_info, "group_info_changed", + G_CALLBACK (eth_group_info_changed), eth); + } + enqueue (eth, -1, eth->nominal_width); + break; + default: + break; + } +} + +static void +eth_get_property (GObject *object, + guint property_id, + GValue *val, + GParamSpec *pspec) +{ + ETableHeader *eth = E_TABLE_HEADER (object); + + switch (property_id) { + case PROP_SORT_INFO: + g_value_set_object (val, eth->sort_info); + break; + case PROP_WIDTH: + g_value_set_double (val, eth->nominal_width); + break; + case PROP_WIDTH_EXTRAS: + g_value_set_double (val, eth->width_extras); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +e_table_header_class_init (ETableHeaderClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = eth_finalize; + object_class->set_property = eth_set_property; + object_class->get_property = eth_get_property; + + g_object_class_install_property ( + object_class, + PROP_WIDTH, + g_param_spec_double ( + "width", "Width", "Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WIDTH_EXTRAS, + g_param_spec_double ( + "width_extras", + "Width of Extras", + "Width of Extras", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SORT_INFO, + g_param_spec_object ( + "sort_info", + "Sort Info", + "Sort Info", + E_TYPE_TABLE_SORT_INFO, + G_PARAM_READWRITE)); + + eth_signals[STRUCTURE_CHANGE] = g_signal_new ( + "structure_change", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableHeaderClass, structure_change), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + eth_signals[DIMENSION_CHANGE] = g_signal_new ( + "dimension_change", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableHeaderClass, dimension_change), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + eth_signals[EXPANSION_CHANGE] = g_signal_new ( + "expansion_change", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableHeaderClass, expansion_change), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + eth_signals[REQUEST_WIDTH] = g_signal_new ( + "request_width", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableHeaderClass, request_width), + (GSignalAccumulator) NULL, NULL, + e_marshal_INT__INT, + G_TYPE_INT, 1, + G_TYPE_INT); + + class->structure_change = NULL; + class->dimension_change = NULL; + class->expansion_change = NULL; + class->request_width = NULL; +} + +static void +e_table_header_init (ETableHeader *eth) +{ + eth->col_count = 0; + eth->width = 0; + + eth->sort_info = NULL; + eth->sort_info_group_change_id = 0; + + eth->columns = NULL; + + eth->change_queue = NULL; + eth->change_tail = NULL; + + eth->width_extras = 0; +} + +/** + * e_table_header_new: + * + * Returns: A new @ETableHeader object. + */ +ETableHeader * +e_table_header_new (void) +{ + + return g_object_new (E_TYPE_TABLE_HEADER, NULL); +} + +static void +eth_update_offsets (ETableHeader *eth) +{ + gint i; + gint x = 0; + + for (i = 0; i < eth->col_count; i++) { + ETableCol *etc = eth->columns[i]; + + etc->x = x; + x += etc->width; + } +} + +static void +eth_do_insert (ETableHeader *eth, + gint pos, + ETableCol *val) +{ + memmove ( + ð->columns[pos + 1], ð->columns[pos], + sizeof (ETableCol *) * (eth->col_count - pos)); + eth->columns[pos] = val; + eth->col_count++; +} + +/** + * e_table_header_add_column: + * @eth: the table header to add the column to. + * @tc: the ETableCol definition + * @pos: position where the ETableCol will go. + * + * This function adds the @tc ETableCol definition into the @eth ETableHeader + * at position @pos. This is the way you add new ETableCols to the + * ETableHeader. The header will assume ownership of the @tc; you should not + * unref it after you add it. + * + * This function will emit the "structure_change" signal on the @eth object. + * The ETableCol is assumed + */ +void +e_table_header_add_column (ETableHeader *eth, + ETableCol *tc, + gint pos) +{ + g_return_if_fail (eth != NULL); + g_return_if_fail (E_IS_TABLE_HEADER (eth)); + g_return_if_fail (tc != NULL); + g_return_if_fail (E_IS_TABLE_COL (tc)); + g_return_if_fail (pos >= -1 && pos <= eth->col_count); + + if (pos == -1) + pos = eth->col_count; + eth->columns = g_realloc ( + eth->columns, sizeof (ETableCol *) * (eth->col_count + 1)); + + /* + * We are the primary owners of the column + */ + g_object_ref (tc); + + eth_do_insert (eth, pos, tc); + + enqueue (eth, -1, eth->nominal_width); + g_signal_emit (eth, eth_signals[STRUCTURE_CHANGE], 0); +} + +/** + * e_table_header_get_column: + * @eth: the ETableHeader to query + * @column: the column inside the @eth. + * + * Returns: The ETableCol at @column in the @eth object + */ +ETableCol * +e_table_header_get_column (ETableHeader *eth, + gint column) +{ + g_return_val_if_fail (eth != NULL, NULL); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), NULL); + + if (column < 0) + return NULL; + + if (column >= eth->col_count) + return NULL; + + return eth->columns[column]; +} + +/** + * e_table_header_get_column_by_col_id: + * @eth: the ETableHeader to query + * @col_id: the col_id to search for. + * + * Returns: The ETableCol with col_idx = @col_idx in the @eth object + */ +ETableCol * +e_table_header_get_column_by_col_idx (ETableHeader *eth, + gint col_idx) +{ + gint i; + g_return_val_if_fail (eth != NULL, NULL); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), NULL); + + for (i = 0; i < eth->col_count; i++) { + if (eth->columns[i]->col_idx == col_idx) { + return eth->columns[i]; + } + } + + return NULL; +} + +/** + * e_table_header_count: + * @eth: the ETableHeader to query + * + * Returns: the number of columns in this ETableHeader. + */ +gint +e_table_header_count (ETableHeader *eth) +{ + g_return_val_if_fail (eth != NULL, 0); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), 0); + + return eth->col_count; +} + +/** + * e_table_header_index: + * @eth: the ETableHeader to query + * @col: the column to fetch. + * + * ETableHeaders contain the visual list of columns that the user will + * view. The visible columns will typically map to different columns + * in the ETableModel (because the user reordered the data for + * example). + * + * Returns: the column in the model that the @col column + * in the ETableHeader points to. */ +gint +e_table_header_index (ETableHeader *eth, + gint col) +{ + g_return_val_if_fail (eth != NULL, -1); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), -1); + g_return_val_if_fail (col >= 0 && col < eth->col_count, -1); + + return eth->columns[col]->col_idx; +} + +/** + * e_table_header_get_index_at: + * @eth: the ETableHeader to query + * @x_offset: a pixel count from the beginning of the ETableHeader + * + * This will return the ETableHeader column that would contain + * the @x_offset pixel. + * + * Returns: the column that contains pixel @x_offset, or -1 + * if no column inside this ETableHeader contains that pixel. + */ +gint +e_table_header_get_index_at (ETableHeader *eth, + gint x_offset) +{ + gint i, total; + + g_return_val_if_fail (eth != NULL, 0); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), 0); + + total = 0; + for (i = 0; i < eth->col_count; i++) { + total += eth->columns[i]->width; + + if (x_offset < total) + return i; + } + + return -1; +} + +/** + * e_table_header_get_columns: + * @eth: The ETableHeader to query + * + * Returns: A NULL terminated array of the ETableCols + * contained in the ETableHeader @eth. Note that every + * returned ETableCol in the array has been referenced, to release + * this information you need to g_free the buffer returned + * and you need to g_object_unref every element returned + */ +ETableCol ** +e_table_header_get_columns (ETableHeader *eth) +{ + ETableCol **ret; + gint i; + + g_return_val_if_fail (eth != NULL, NULL); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), NULL); + + ret = g_new (ETableCol *, eth->col_count + 1); + memcpy (ret, eth->columns, sizeof (ETableCol *) * eth->col_count); + ret[eth->col_count] = NULL; + + for (i = 0; i < eth->col_count; i++) { + g_object_ref (ret[i]); + } + + return ret; +} + +/** + * e_table_header_get_selected: + * @eth: The ETableHeader to query + * + * Returns: The number of selected columns in the @eth object. + */ +gint +e_table_header_get_selected (ETableHeader *eth) +{ + gint i; + gint selected = 0; + + g_return_val_if_fail (eth != NULL, 0); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), 0); + + for (i = 0; i < eth->col_count; i++) { + if (eth->columns[i]->selected) + selected++; + } + + return selected; +} + +/** + * e_table_header_total_width: + * @eth: The ETableHeader to query + * + * Returns: the number of pixels used by the @eth object + * when rendered on screen + */ +gint +e_table_header_total_width (ETableHeader *eth) +{ + gint total, i; + + g_return_val_if_fail (eth != NULL, 0); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), 0); + + total = 0; + for (i = 0; i < eth->col_count; i++) + total += eth->columns[i]->width; + + return total; +} + +/** + * e_table_header_min_width: + * @eth: The ETableHeader to query + * + * Returns: the minimum number of pixels required by the @eth object. + **/ +gint +e_table_header_min_width (ETableHeader *eth) +{ + gint total, i; + + g_return_val_if_fail (eth != NULL, 0); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), 0); + + total = 0; + for (i = 0; i < eth->col_count; i++) + total += eth->columns[i]->min_width; + + return total; +} + +/** + * e_table_header_move: + * @eth: The ETableHeader to operate on. + * @source_index: the source column to move. + * @target_index: the target location for the column + * + * This function moves the column @source_index to @target_index + * inside the @eth ETableHeader. The signals "dimension_change" + * and "structure_change" will be emmited + */ +void +e_table_header_move (ETableHeader *eth, + gint source_index, + gint target_index) +{ + ETableCol *old; + + g_return_if_fail (eth != NULL); + g_return_if_fail (E_IS_TABLE_HEADER (eth)); + g_return_if_fail (source_index >= 0); + g_return_if_fail (target_index >= 0); + g_return_if_fail (source_index < eth->col_count); + + /* Can be moved beyond the last item. */ + g_return_if_fail (target_index < eth->col_count + 1); + + if (source_index < target_index) + target_index--; + + old = eth->columns[source_index]; + eth_do_remove (eth, source_index, FALSE); + eth_do_insert (eth, target_index, old); + eth_update_offsets (eth); + + g_signal_emit (eth, eth_signals[DIMENSION_CHANGE], 0, eth->width); + g_signal_emit (eth, eth_signals[STRUCTURE_CHANGE], 0); +} + +/** + * e_table_header_remove: + * @eth: The ETableHeader to operate on. + * @idx: the index to the column to be removed. + * + * Removes the column at @idx position in the ETableHeader @eth. + * This emmits the "structure_change" signal on the @eth object. + */ +void +e_table_header_remove (ETableHeader *eth, + gint idx) +{ + g_return_if_fail (eth != NULL); + g_return_if_fail (E_IS_TABLE_HEADER (eth)); + g_return_if_fail (idx >= 0); + g_return_if_fail (idx < eth->col_count); + + eth_do_remove (eth, idx, TRUE); + enqueue (eth, -1, eth->nominal_width); + g_signal_emit (eth, eth_signals[STRUCTURE_CHANGE], 0); +} + +/* + * FIXME: deprecated? + */ +void +e_table_header_set_selection (ETableHeader *eth, + gboolean allow_selection) +{ + g_return_if_fail (eth != NULL); + g_return_if_fail (E_IS_TABLE_HEADER (eth)); +} + +static void +eth_set_size (ETableHeader *eth, + gint idx, + gint size) +{ + gdouble expansion; + gdouble old_expansion; + gint min_width; + gint left_width; + gint total_extra; + gint expandable_count; + gint usable_width; + gint i; + g_return_if_fail (eth != NULL); + g_return_if_fail (E_IS_TABLE_HEADER (eth)); + g_return_if_fail (idx >= 0); + g_return_if_fail (idx < eth->col_count); + + /* If this column is not resizable, don't do anything. */ + if (!eth->columns[idx]->resizable) + return; + + expansion = 0; + min_width = 0; + left_width = 0; + expandable_count = -1; + + /* Calculate usable area. */ + for (i = 0; i < idx; i++) { + left_width += eth->columns[i]->width; + } + /* - 1 to account for the last pixel border. */ + usable_width = eth->width - left_width - 1; + + if (eth->sort_info) + usable_width -= e_table_sort_info_grouping_get_count ( + eth->sort_info) * GROUP_INDENT; + + /* Calculate minimum_width of stuff on the right as well as + * total usable expansion on the right. + */ + for (; i < eth->col_count; i++) { + min_width += eth->columns[i]->min_width + eth->width_extras; + if (eth->columns[i]->resizable) { + expansion += eth->columns[i]->expansion; + expandable_count++; + } + } + /* If there's no room for anything, don't change. */ + if (expansion == 0) + return; + + /* (1) If none of the columns to the right are expandable, use + * all the expansion space in this column. + */ + if (expandable_count == 0) { + eth->columns[idx]->expansion = expansion; + for (i = idx + 1; i < eth->col_count; i++) { + eth->columns[i]->expansion = 0; + } + + g_signal_emit (eth, eth_signals[EXPANSION_CHANGE], 0); + return; + } + + total_extra = usable_width - min_width; + /* If there's no extra space, set all expansions to 0. */ + if (total_extra <= 0) { + for (i = idx; i < eth->col_count; i++) { + eth->columns[i]->expansion = 0; + } + g_signal_emit (eth, eth_signals[EXPANSION_CHANGE], 0); + return; + } + + /* If you try to resize smaller than the minimum width, it + * uses the minimum. */ + if (size < eth->columns[idx]->min_width + eth->width_extras) + size = eth->columns[idx]->min_width + eth->width_extras; + + /* If all the extra space will be used up in this column, use + * all the expansion and set all others to 0. + */ + if (size >= total_extra + eth->columns[idx]->min_width + eth->width_extras) { + eth->columns[idx]->expansion = expansion; + for (i = idx + 1; i < eth->col_count; i++) { + eth->columns[i]->expansion = 0; + } + g_signal_emit (eth, eth_signals[EXPANSION_CHANGE], 0); + return; + } + + /* The old_expansion used by columns to the right. */ + old_expansion = expansion; + old_expansion -= eth->columns[idx]->expansion; + /* Set the new expansion so that it will generate the desired size. */ + eth->columns[idx]->expansion = + expansion * (((gdouble)(size - (eth->columns[idx]->min_width + + eth->width_extras))) / ((gdouble) total_extra)); + /* The expansion left for the columns on the right. */ + expansion -= eth->columns[idx]->expansion; + + /* (2) If the old columns to the right didn't have any + * expansion before, expand them evenly. old_expansion > 0 by + * expansion = SUM(i=idx to col_count -1, + * columns[i]->min_width) - columns[idx]->min_width) = + * SUM(non-negatives). + */ + if (old_expansion == 0) { + for (i = idx + 1; i < eth->col_count; i++) { + if (eth->columns[idx]->resizable) { + /* expandable_count != 0 by (1) */ + eth->columns[i]->expansion = expansion / expandable_count; + } + } + g_signal_emit (eth, eth_signals[EXPANSION_CHANGE], 0); + return; + } + + for (i = idx + 1; i < eth->col_count; i++) { + if (eth->columns[idx]->resizable) { + /* old_expansion != 0 by (2) */ + eth->columns[i]->expansion *= expansion / old_expansion; + } + } + g_signal_emit (eth, eth_signals[EXPANSION_CHANGE], 0); +} + +/** + * e_table_header_col_diff: + * @eth: the ETableHeader to query. + * @start_col: the starting column + * @end_col: the ending column. + * + * Computes the number of pixels between the columns @start_col and + * @end_col. + * + * Returns: the number of pixels between @start_col and @end_col on the + * @eth ETableHeader object + */ +gint +e_table_header_col_diff (ETableHeader *eth, + gint start_col, + gint end_col) +{ + gint total, col; + + g_return_val_if_fail (eth != NULL, 0); + g_return_val_if_fail (E_IS_TABLE_HEADER (eth), 0); + + if (start_col < 0) + start_col = 0; + if (end_col > eth->col_count) + end_col = eth->col_count; + + total = 0; + for (col = start_col; col < end_col; col++) { + + total += eth->columns[col]->width; + } + + return total; +} + +static void +eth_calc_widths (ETableHeader *eth) +{ + gint i; + gint extra; + gdouble expansion; + gint last_position = 0; + gdouble next_position = 0; + gint last_resizable = -1; + gint *widths; + gboolean changed; + + widths = g_new (int, eth->col_count); + + /* - 1 to account for the last pixel border. */ + extra = eth->width - 1; + expansion = 0; + for (i = 0; i < eth->col_count; i++) { + extra -= eth->columns[i]->min_width + eth->width_extras; + if (eth->columns[i]->resizable && eth->columns[i]->expansion > 0) + last_resizable = i; + expansion += eth->columns[i]->resizable ? eth->columns[i]->expansion : 0; + widths[i] = eth->columns[i]->min_width + eth->width_extras; + } + if (eth->sort_info) + extra -= e_table_sort_info_grouping_get_count (eth->sort_info) + * GROUP_INDENT; + if (expansion != 0 && extra > 0) { + for (i = 0; i < last_resizable; i++) { + next_position += + extra * (eth->columns[i]->resizable ? + eth->columns[i]->expansion : 0) / expansion; + widths[i] += next_position - last_position; + last_position = next_position; + } + widths[i] += extra - last_position; + } + + changed = FALSE; + + for (i = 0; i < eth->col_count; i++) { + if (eth->columns[i]->width != widths[i]) { + changed = TRUE; + eth->columns[i]->width = widths[i]; + } + } + g_free (widths); + if (changed) + g_signal_emit (eth, eth_signals[DIMENSION_CHANGE], 0, eth->width); + eth_update_offsets (eth); +} + +void +e_table_header_update_horizontal (ETableHeader *eth) +{ + gint i; + gint cols; + + cols = eth->col_count; + + for (i = 0; i < cols; i++) { + gint width = 0; + + g_signal_emit_by_name ( + eth, "request_width", i, &width); + eth->columns[i]->min_width = width + 10; + eth->columns[i]->expansion = 1; + } + enqueue (eth, -1, eth->nominal_width); + g_signal_emit (eth, eth_signals[EXPANSION_CHANGE], 0); +} + +gint +e_table_header_prioritized_column (ETableHeader *eth) +{ + gint best_model_col = 0; + gint best_priority; + gint i; + gint count; + + count = e_table_header_count (eth); + if (count == 0) + return -1; + best_priority = e_table_header_get_column (eth, 0)->priority; + best_model_col = e_table_header_get_column (eth, 0)->col_idx; + for (i = 1; i < count; i++) { + gint priority = e_table_header_get_column (eth, i)->priority; + if (priority > best_priority) { + best_priority = priority; + best_model_col = e_table_header_get_column (eth, i)->col_idx; + } + } + return best_model_col; +} + +ETableCol * +e_table_header_prioritized_column_selected (ETableHeader *eth, + ETableColCheckFunc check_func, + gpointer user_data) +{ + ETableCol *best_col = NULL; + gint best_priority = G_MININT; + gint i; + gint count; + + count = e_table_header_count (eth); + if (count == 0) + return NULL; + for (i = 1; i < count; i++) { + ETableCol *col = e_table_header_get_column (eth, i); + if (col) { + if ((best_col == NULL || col->priority > best_priority) + && check_func (col, user_data)) { + best_priority = col->priority; + best_col = col; + } + } + } + return best_col; +} diff --git a/e-util/e-table-header.h b/e-util/e-table-header.h new file mode 100644 index 0000000000..298131eeed --- /dev/null +++ b/e-util/e-table-header.h @@ -0,0 +1,144 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_COLUMN_H_ +#define _E_TABLE_COLUMN_H_ + +#include <gdk/gdk.h> + +#include <e-util/e-table-col.h> +#include <e-util/e-table-sort-info.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_HEADER \ + (e_table_header_get_type ()) +#define E_TABLE_HEADER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_HEADER, ETableHeader)) +#define E_TABLE_HEADER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_HEADER, ETableHeaderClass)) +#define E_IS_TABLE_HEADER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_HEADER)) +#define E_IS_TABLE_HEADER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_HEADER)) +#define E_TABLE_HEADER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_HEADER, ETableHeaderClass)) + +G_BEGIN_DECLS + +typedef struct _ETableHeader ETableHeader; +typedef struct _ETableHeaderClass ETableHeaderClass; + +typedef gboolean (*ETableColCheckFunc) (ETableCol *col, gpointer user_data); + +/* + * A Column header. + */ +struct _ETableHeader { + GObject parent; + + gint col_count; + gint width; + gint nominal_width; + gint width_extras; + + ETableSortInfo *sort_info; + gint sort_info_group_change_id; + + ETableCol **columns; + + GSList *change_queue, *change_tail; + gint idle; +}; + +struct _ETableHeaderClass { + GObjectClass parent_class; + + void (*structure_change) (ETableHeader *eth); + void (*dimension_change) (ETableHeader *eth, + gint width); + void (*expansion_change) (ETableHeader *eth); + gint (*request_width) (ETableHeader *eth, + gint col); +}; + +GType e_table_header_get_type (void) G_GNUC_CONST; +ETableHeader * e_table_header_new (void); + +void e_table_header_add_column (ETableHeader *eth, + ETableCol *tc, + gint pos); +ETableCol * e_table_header_get_column (ETableHeader *eth, + gint column); +ETableCol * e_table_header_get_column_by_col_idx + (ETableHeader *eth, + gint col_idx); +gint e_table_header_count (ETableHeader *eth); +gint e_table_header_index (ETableHeader *eth, + gint col); +gint e_table_header_get_index_at (ETableHeader *eth, + gint x_offset); +ETableCol ** e_table_header_get_columns (ETableHeader *eth); +gint e_table_header_get_selected (ETableHeader *eth); + +gint e_table_header_total_width (ETableHeader *eth); +gint e_table_header_min_width (ETableHeader *eth); +void e_table_header_move (ETableHeader *eth, + gint source_index, + gint target_index); +void e_table_header_remove (ETableHeader *eth, + gint idx); +void e_table_header_set_size (ETableHeader *eth, + gint idx, + gint size); +void e_table_header_set_selection (ETableHeader *eth, + gboolean allow_selection); +gint e_table_header_col_diff (ETableHeader *eth, + gint start_col, + gint end_col); + +void e_table_header_calc_widths (ETableHeader *eth); +GList * e_table_header_get_selected_indexes + (ETableHeader *eth); +void e_table_header_update_horizontal + (ETableHeader *eth); +gint e_table_header_prioritized_column + (ETableHeader *eth); +ETableCol * e_table_header_prioritized_column_selected + (ETableHeader *eth, + ETableColCheckFunc check_func, + gpointer user_data); + +G_END_DECLS + +#endif /* _E_TABLE_HEADER_H_ */ + diff --git a/e-util/e-table-item.c b/e-util/e-table-item.c new file mode 100644 index 0000000000..de749ead68 --- /dev/null +++ b/e-util/e-table-item.c @@ -0,0 +1,4041 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * e-table-item.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@gnu.org> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ +/* + * TODO: + * Add a border to the thing, so that focusing works properly. + */ +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-item.h" + +#include <math.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include "e-canvas-utils.h" +#include "e-canvas.h" +#include "e-cell.h" +#include "e-marshal.h" +#include "e-table-subset.h" +#include "gal-a11y-e-table-item-factory.h" +#include "gal-a11y-e-table-item.h" + +/* workaround for avoiding API breakage */ +#define eti_get_type e_table_item_get_type +G_DEFINE_TYPE (ETableItem, eti, GNOME_TYPE_CANVAS_ITEM) + +#define FOCUSED_BORDER 2 + +#define d(x) + +#if d(!)0 +#define e_table_item_leave_edit_(x) (e_table_item_leave_edit((x)), g_print ("%s: e_table_item_leave_edit\n", __FUNCTION__)) +#else +#define e_table_item_leave_edit_(x) (e_table_item_leave_edit((x))) +#endif + +static void eti_check_cursor_bounds (ETableItem *eti); +static void eti_cancel_drag_due_to_model_change (ETableItem *eti); + +/* FIXME: Do an analysis of which cell functions are needed before + * realize and make sure that all of them are doable by all the cells + * and that all of the others are only done after realization. */ + +enum { + CURSOR_CHANGE, + CURSOR_ACTIVATED, + DOUBLE_CLICK, + RIGHT_CLICK, + CLICK, + KEY_PRESS, + START_DRAG, + STYLE_SET, + SELECTION_MODEL_REMOVED, + SELECTION_MODEL_ADDED, + LAST_SIGNAL +}; + +static guint eti_signals[LAST_SIGNAL] = { 0, }; + +enum { + PROP_0, + PROP_TABLE_HEADER, + PROP_TABLE_MODEL, + PROP_SELECTION_MODEL, + PROP_TABLE_ALTERNATING_ROW_COLORS, + PROP_TABLE_HORIZONTAL_DRAW_GRID, + PROP_TABLE_VERTICAL_DRAW_GRID, + PROP_TABLE_DRAW_FOCUS, + PROP_CURSOR_MODE, + PROP_LENGTH_THRESHOLD, + PROP_CURSOR_ROW, + PROP_UNIFORM_ROW_HEIGHT, + + PROP_MINIMUM_WIDTH, + PROP_WIDTH, + PROP_HEIGHT +}; + +#define DOUBLE_CLICK_TIME 250 +#define TRIPLE_CLICK_TIME 500 + +static gint eti_get_height (ETableItem *eti); +static gint eti_row_height (ETableItem *eti, gint row); +static void e_table_item_focus (ETableItem *eti, gint col, gint row, GdkModifierType state); +static void eti_cursor_change (ESelectionModel *selection, gint row, gint col, ETableItem *eti); +static void eti_cursor_activated (ESelectionModel *selection, gint row, gint col, ETableItem *eti); +static void eti_selection_change (ESelectionModel *selection, ETableItem *eti); +static void eti_selection_row_change (ESelectionModel *selection, gint row, ETableItem *eti); +static void e_table_item_redraw_row (ETableItem *eti, gint row); + +#define ETI_SINGLE_ROW_HEIGHT(eti) ((eti)->uniform_row_height_cache != -1 ? (eti)->uniform_row_height_cache : eti_row_height((eti), -1)) +#define ETI_MULTIPLE_ROW_HEIGHT(eti,row) ((eti)->height_cache && (eti)->height_cache[(row)] != -1 ? (eti)->height_cache[(row)] : eti_row_height((eti),(row))) +#define ETI_ROW_HEIGHT(eti,row) ((eti)->uniform_row_height ? ETI_SINGLE_ROW_HEIGHT ((eti)) : ETI_MULTIPLE_ROW_HEIGHT((eti),(row))) + +/* tweak_hsv is a really tweaky function. it modifies its first argument, which + * should be the color you want tweaked. delta_h, delta_s and delta_v specify + * how much you want their respective channels modified (and in what direction). + * if it can't do the specified modification, it does it in the oppositon direction */ +static void +e_hsv_tweak (GdkColor *color, + gdouble delta_h, + gdouble delta_s, + gdouble delta_v) +{ + gdouble h, s, v, r, g, b; + + r = color->red / 65535.0f; + g = color->green / 65535.0f; + b = color->blue / 65535.0f; + + gtk_rgb_to_hsv (r, g, b, &h, &s, &v); + + if (h + delta_h < 0) { + h -= delta_h; + } else { + h += delta_h; + } + + if (s + delta_s < 0) { + s -= delta_s; + } else { + s += delta_s; + } + + if (v + delta_v < 0) { + v -= delta_v; + } else { + v += delta_v; + } + + gtk_hsv_to_rgb (h, s, v, &r, &g, &b); + + color->red = r * 65535.0f; + color->green = g * 65535.0f; + color->blue = b * 65535.0f; +} + +inline static gint +model_to_view_row (ETableItem *eti, + gint row) +{ + gint i; + if (row == -1) + return -1; + if (eti->uses_source_model) { + ETableSubset *etss = E_TABLE_SUBSET (eti->table_model); + if (eti->row_guess >= 0 && eti->row_guess < etss->n_map) { + if (etss->map_table[eti->row_guess] == row) { + return eti->row_guess; + } + } + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] == row) + return i; + } + return -1; + } else + return row; +} + +inline static gint +view_to_model_row (ETableItem *eti, + gint row) +{ + if (eti->uses_source_model) { + ETableSubset *etss = E_TABLE_SUBSET (eti->table_model); + if (row >= 0 && row < etss->n_map) { + eti->row_guess = row; + return etss->map_table[row]; + } else + return -1; + } else + return row; +} + +inline static gint +model_to_view_col (ETableItem *eti, + gint col) +{ + gint i; + if (col == -1) + return -1; + for (i = 0; i < eti->cols; i++) { + ETableCol *ecol = e_table_header_get_column (eti->header, i); + if (ecol->col_idx == col) + return i; + } + return -1; +} + +inline static gint +view_to_model_col (ETableItem *eti, + gint col) +{ + ETableCol *ecol = e_table_header_get_column (eti->header, col); + return ecol ? ecol->col_idx : -1; +} + +static void +grab_cancelled (ECanvas *canvas, + GnomeCanvasItem *item, + gpointer data) +{ + ETableItem *eti = data; + + eti->grab_cancelled = TRUE; +} + +inline static void +eti_grab (ETableItem *eti, + GdkDevice *device, + guint32 time) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + d (g_print ("%s: time: %d\n", __FUNCTION__, time)); + if (eti->grabbed_count == 0) { + GdkGrabStatus grab_status; + + eti->gtk_grabbed = FALSE; + eti->grab_cancelled = FALSE; + + grab_status = e_canvas_item_grab ( + E_CANVAS (item->canvas), + item, + GDK_BUTTON1_MOTION_MASK | + GDK_BUTTON2_MOTION_MASK | + GDK_BUTTON3_MOTION_MASK | + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK, + NULL, + device, time, + grab_cancelled, + eti); + + if (grab_status != GDK_GRAB_SUCCESS) { + d (g_print ("%s: gtk_grab_add\n", __FUNCTION__)); + gtk_grab_add (GTK_WIDGET (item->canvas)); + eti->gtk_grabbed = TRUE; + } + } + eti->grabbed_count++; +} + +inline static void +eti_ungrab (ETableItem *eti, + guint32 time) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + d (g_print ("%s: time: %d\n", __FUNCTION__, time)); + eti->grabbed_count--; + if (eti->grabbed_count == 0) { + if (eti->grab_cancelled) { + eti->grab_cancelled = FALSE; + } else { + if (eti->gtk_grabbed) { + d (g_print ("%s: gtk_grab_remove\n", __FUNCTION__)); + gtk_grab_remove (GTK_WIDGET (item->canvas)); + eti->gtk_grabbed = FALSE; + } + gnome_canvas_item_ungrab (item, time); + eti->grabbed_col = -1; + eti->grabbed_row = -1; + } + } +} + +inline static gboolean +eti_editing (ETableItem *eti) +{ + d (g_print ("%s: %s\n", __FUNCTION__, (eti->editing_col == -1) ? "false":"true")); + + if (eti->editing_col == -1) + return FALSE; + else + return TRUE; +} + +inline static GdkColor * +eti_get_cell_background_color (ETableItem *eti, + gint row, + gint col, + gboolean selected, + gboolean *allocatedp) +{ + ECellView *ecell_view = eti->cell_views[col]; + GtkWidget *canvas; + GdkColor *background, bg; + GtkStyle *style; + gchar *color_spec = NULL; + gboolean allocated = FALSE; + + canvas = GTK_WIDGET (GNOME_CANVAS_ITEM (eti)->canvas); + style = gtk_widget_get_style (canvas); + + if (selected) { + if (gtk_widget_has_focus (canvas)) + background = &style->bg[GTK_STATE_SELECTED]; + else + background = &style->bg[GTK_STATE_ACTIVE]; + } else { + background = &style->base[GTK_STATE_NORMAL]; + } + + color_spec = e_cell_get_bg_color (ecell_view, row); + + if (color_spec != NULL) { + if (gdk_color_parse (color_spec, &bg)) { + background = gdk_color_copy (&bg); + allocated = TRUE; + } + } + + if (eti->alternating_row_colors) { + if (row % 2) { + + } else { + if (!allocated) { + background = gdk_color_copy (background); + allocated = TRUE; + } + e_hsv_tweak (background, 0.0f, 0.0f, -0.07f); + } + } + if (allocatedp) + *allocatedp = allocated; + + return background; +} + +inline static GdkColor * +eti_get_cell_foreground_color (ETableItem *eti, + gint row, + gint col, + gboolean selected, + gboolean *allocated) +{ + GtkWidget *canvas; + GdkColor *foreground; + GtkStyle *style; + + canvas = GTK_WIDGET (GNOME_CANVAS_ITEM (eti)->canvas); + style = gtk_widget_get_style (canvas); + + if (allocated) + *allocated = FALSE; + + if (selected) { + if (gtk_widget_has_focus (canvas)) + foreground = &style->fg[GTK_STATE_SELECTED]; + else + foreground = &style->fg[GTK_STATE_ACTIVE]; + } else { + foreground = &style->text[GTK_STATE_NORMAL]; + } + + return foreground; +} + +static void +eti_free_save_state (ETableItem *eti) +{ + if (eti->save_row == -1 || + !eti->cell_views_realized) + return; + + e_cell_free_state ( + eti->cell_views[eti->save_col], view_to_model_col (eti, eti->save_col), + eti->save_col, eti->save_row, eti->save_state); + eti->save_row = -1; + eti->save_col = -1; + eti->save_state = NULL; +} + +/* + * During realization, we have to invoke the per-ecell realize routine + * (On our current setup, we have one e-cell per column. + * + * We might want to optimize this to only realize the unique e-cells: + * ie, a strings-only table, uses the same e-cell for every column, and + * we might want to avoid realizing each e-cell. + */ +static void +eti_realize_cell_views (ETableItem *eti) +{ + GnomeCanvasItem *item; + gint i; + + item = GNOME_CANVAS_ITEM (eti); + + if (eti->cell_views_realized) + return; + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) + return; + + for (i = 0; i < eti->n_cells; i++) + e_cell_realize (eti->cell_views[i]); + eti->cell_views_realized = 1; +} + +static void +eti_attach_cell_views (ETableItem *eti) +{ + gint i; + + g_return_if_fail (eti->header); + g_return_if_fail (eti->table_model); + + /* this is just c&p from model pre change, but it fixes things */ + eti_cancel_drag_due_to_model_change (eti); + eti_check_cursor_bounds (eti); + if (eti_editing (eti)) + e_table_item_leave_edit_(eti); + eti->motion_row = -1; + eti->motion_col = -1; + + /* + * Now realize the various ECells + */ + eti->n_cells = eti->cols; + eti->cell_views = g_new (ECellView *, eti->n_cells); + + for (i = 0; i < eti->n_cells; i++) { + ETableCol *ecol = e_table_header_get_column (eti->header, i); + + eti->cell_views[i] = e_cell_new_view (ecol->ecell, eti->table_model, eti); + } + + eti->needs_compute_height = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); +} + +/* + * During unrealization: we invoke every e-cell (one per column in the current + * setup) to dispose all X resources allocated + */ +static void +eti_unrealize_cell_views (ETableItem *eti) +{ + gint i; + + if (eti->cell_views_realized == 0) + return; + + eti_free_save_state (eti); + + for (i = 0; i < eti->n_cells; i++) + e_cell_unrealize (eti->cell_views[i]); + eti->cell_views_realized = 0; +} + +static void +eti_detach_cell_views (ETableItem *eti) +{ + gint i; + + eti_free_save_state (eti); + + for (i = 0; i < eti->n_cells; i++) { + e_cell_kill_view (eti->cell_views[i]); + eti->cell_views[i] = NULL; + } + + g_free (eti->cell_views); + eti->cell_views = NULL; + eti->n_cells = 0; +} + +static void +eti_bounds (GnomeCanvasItem *item, + gdouble *x1, + gdouble *y1, + gdouble *x2, + gdouble *y2) +{ + cairo_matrix_t i2c; + ETableItem *eti = E_TABLE_ITEM (item); + + /* Wrong BBox's are the source of redraw nightmares */ + + *x1 = 0; + *y1 = 0; + *x2 = eti->width; + *y2 = eti->height; + + gnome_canvas_item_i2c_matrix (GNOME_CANVAS_ITEM (eti), &i2c); + gnome_canvas_matrix_transform_rect (&i2c, x1, y1, x2, y2); +} + +static void +eti_reflow (GnomeCanvasItem *item, + gint flags) +{ + ETableItem *eti = E_TABLE_ITEM (item); + + if (eti->needs_compute_height) { + gint new_height = eti_get_height (eti); + + if (new_height != eti->height) { + eti->height = new_height; + e_canvas_item_request_parent_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); + } + eti->needs_compute_height = 0; + } + if (eti->needs_compute_width) { + gint new_width = e_table_header_total_width (eti->header); + if (new_width != eti->width) { + eti->width = new_width; + e_canvas_item_request_parent_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); + } + eti->needs_compute_width = 0; + } +} + +/* + * GnomeCanvasItem::update method + */ +static void +eti_update (GnomeCanvasItem *item, + const cairo_matrix_t *i2c, + gint flags) +{ + ETableItem *eti = E_TABLE_ITEM (item); + gdouble x1, x2, y1, y2; + + if (GNOME_CANVAS_ITEM_CLASS (eti_parent_class)->update) + (*GNOME_CANVAS_ITEM_CLASS (eti_parent_class)->update)(item, i2c, flags); + + x1 = item->x1; + y1 = item->y1; + x2 = item->x2; + y2 = item->y2; + + eti_bounds (item, &item->x1, &item->y1, &item->x2, &item->y2); + if (item->x1 != x1 || + item->y1 != y1 || + item->x2 != x2 || + item->y2 != y2) { + gnome_canvas_request_redraw (item->canvas, x1, y1, x2, y2); + eti->needs_redraw = 1; + } + + if (eti->needs_redraw) { + gnome_canvas_request_redraw ( + item->canvas, item->x1, item->y1, + item->x2, item->y2); + eti->needs_redraw = 0; + } +} + +/* + * eti_remove_table_model: + * + * Invoked to release the table model associated with this ETableItem + */ +static void +eti_remove_table_model (ETableItem *eti) +{ + if (!eti->table_model) + return; + + g_signal_handler_disconnect ( + eti->table_model, + eti->table_model_pre_change_id); + g_signal_handler_disconnect ( + eti->table_model, + eti->table_model_no_change_id); + g_signal_handler_disconnect ( + eti->table_model, + eti->table_model_change_id); + g_signal_handler_disconnect ( + eti->table_model, + eti->table_model_row_change_id); + g_signal_handler_disconnect ( + eti->table_model, + eti->table_model_cell_change_id); + g_signal_handler_disconnect ( + eti->table_model, + eti->table_model_rows_inserted_id); + g_signal_handler_disconnect ( + eti->table_model, + eti->table_model_rows_deleted_id); + g_object_unref (eti->table_model); + if (eti->source_model) + g_object_unref (eti->source_model); + + eti->table_model_pre_change_id = 0; + eti->table_model_no_change_id = 0; + eti->table_model_change_id = 0; + eti->table_model_row_change_id = 0; + eti->table_model_cell_change_id = 0; + eti->table_model_rows_inserted_id = 0; + eti->table_model_rows_deleted_id = 0; + eti->table_model = NULL; + eti->source_model = NULL; + eti->uses_source_model = 0; +} + +/* + * eti_remove_table_model: + * + * Invoked to release the table model associated with this ETableItem + */ +static void +eti_remove_selection_model (ETableItem *eti) +{ + if (!eti->selection) + return; + + g_signal_handler_disconnect ( + eti->selection, + eti->selection_change_id); + g_signal_handler_disconnect ( + eti->selection, + eti->selection_row_change_id); + g_signal_handler_disconnect ( + eti->selection, + eti->cursor_change_id); + g_signal_handler_disconnect ( + eti->selection, + eti->cursor_activated_id); + g_object_unref (eti->selection); + + eti->selection_change_id = 0; + eti->selection_row_change_id = 0; + eti->cursor_activated_id = 0; + eti->selection = NULL; +} + +/* + * eti_remove_header_model: + * + * Invoked to release the header model associated with this ETableItem + */ +static void +eti_remove_header_model (ETableItem *eti) +{ + if (!eti->header) + return; + + g_signal_handler_disconnect ( + eti->header, + eti->header_structure_change_id); + g_signal_handler_disconnect ( + eti->header, + eti->header_dim_change_id); + g_signal_handler_disconnect ( + eti->header, + eti->header_request_width_id); + + if (eti->cell_views) { + eti_unrealize_cell_views (eti); + eti_detach_cell_views (eti); + } + g_object_unref (eti->header); + + eti->header_structure_change_id = 0; + eti->header_dim_change_id = 0; + eti->header_request_width_id = 0; + eti->header = NULL; +} + +/* + * eti_row_height_real: + * + * Returns the height used by row @row. This does not include the one-pixel + * used as a separator between rows + */ +static gint +eti_row_height_real (ETableItem *eti, + gint row) +{ + const gint cols = e_table_header_count (eti->header); + gint col; + gint h, max_h; + + g_return_val_if_fail (cols == 0 || eti->cell_views, 0); + + max_h = 0; + + for (col = 0; col < cols; col++) { + h = e_cell_height (eti->cell_views[col], view_to_model_col (eti, col), col, row); + + if (h > max_h) + max_h = h; + } + return max_h; +} + +static void +confirm_height_cache (ETableItem *eti) +{ + gint i; + + if (eti->uniform_row_height || eti->height_cache) + return; + eti->height_cache = g_new (int, eti->rows); + for (i = 0; i < eti->rows; i++) { + eti->height_cache[i] = -1; + } +} + +static gboolean +height_cache_idle (ETableItem *eti) +{ + gint changed = 0; + gint i; + confirm_height_cache (eti); + for (i = eti->height_cache_idle_count; i < eti->rows; i++) { + if (eti->height_cache[i] == -1) { + eti_row_height (eti, i); + changed++; + if (changed >= 20) + break; + } + } + if (changed >= 20) { + eti->height_cache_idle_count = i; + return TRUE; + } + eti->height_cache_idle_id = 0; + return FALSE; +} + +static void +free_height_cache (ETableItem *eti) +{ + GnomeCanvasItem *item; + + item = GNOME_CANVAS_ITEM (eti); + + if (item->flags & GNOME_CANVAS_ITEM_REALIZED) { + if (eti->height_cache) + g_free (eti->height_cache); + eti->height_cache = NULL; + eti->height_cache_idle_count = 0; + eti->uniform_row_height_cache = -1; + + if (eti->uniform_row_height && eti->height_cache_idle_id != 0) { + g_source_remove (eti->height_cache_idle_id); + eti->height_cache_idle_id = 0; + } + + if ((!eti->uniform_row_height) && eti->height_cache_idle_id == 0) + eti->height_cache_idle_id = g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc) height_cache_idle, eti, NULL); + } +} + +static void +calculate_height_cache (ETableItem *eti) +{ + free_height_cache (eti); + confirm_height_cache (eti); +} + +/* + * eti_row_height: + * + * Returns the height used by row @row. This does not include the one-pixel + * used as a separator between rows + */ +static gint +eti_row_height (ETableItem *eti, + gint row) +{ + if (eti->uniform_row_height) { + eti->uniform_row_height_cache = eti_row_height_real (eti, -1); + return eti->uniform_row_height_cache; + } else { + if (!eti->height_cache) { + calculate_height_cache (eti); + } + if (eti->height_cache[row] == -1) { + eti->height_cache[row] = eti_row_height_real (eti, row); + if (row > 0 && + eti->length_threshold != -1 && + eti->rows > eti->length_threshold && + eti->height_cache[row] != eti_row_height (eti, 0)) { + eti->needs_compute_height = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (eti)); + } + } + return eti->height_cache[row]; + } +} + +/* + * eti_get_height: + * + * Returns the height of the ETableItem. + * + * The ETableItem might compute the whole height by asking every row its + * size. There is a special mode (designed to work when there are too + * many rows in the table that performing the previous step could take + * too long) set by the ETableItem->length_threshold that would determine + * when the height is computed by using the first row as the size for + * every other row in the ETableItem. + */ +static gint +eti_get_height (ETableItem *eti) +{ + const gint rows = eti->rows; + gint height_extra = eti->horizontal_draw_grid ? 1 : 0; + + if (rows == 0) + return 0; + + if (eti->uniform_row_height) { + gint row_height = ETI_ROW_HEIGHT (eti, -1); + return ((row_height + height_extra) * rows + height_extra); + } else { + gint height; + gint row; + if (eti->length_threshold != -1) { + if (rows > eti->length_threshold) { + gint row_height = ETI_ROW_HEIGHT (eti, 0); + if (eti->height_cache) { + height = 0; + for (row = 0; row < rows; row++) { + if (eti->height_cache[row] == -1) { + height += (row_height + height_extra) * (rows - row); + break; + } + else + height += eti->height_cache[row] + height_extra; + } + } else + height = (ETI_ROW_HEIGHT (eti, 0) + height_extra) * rows; + + /* + * 1 pixel at the top + */ + return height + height_extra; + } + } + + height = height_extra; + for (row = 0; row < rows; row++) + height += ETI_ROW_HEIGHT (eti, row) + height_extra; + + return height; + } +} + +static void +eti_item_region_redraw (ETableItem *eti, + gint x0, + gint y0, + gint x1, + gint y1) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + gdouble dx1, dy1, dx2, dy2; + cairo_matrix_t i2c; + + dx1 = x0; + dy1 = y0; + dx2 = x1; + dy2 = y1; + + gnome_canvas_item_i2c_matrix (item, &i2c); + gnome_canvas_matrix_transform_rect (&i2c, &dx1, &dy1, &dx2, &dy2); + + gnome_canvas_request_redraw (item->canvas, floor (dx1), floor (dy1), ceil (dx2), ceil (dy2)); +} + +/* + * Computes the distance between @start_row and @end_row in pixels + */ +gint +e_table_item_row_diff (ETableItem *eti, + gint start_row, + gint end_row) +{ + gint height_extra = eti->horizontal_draw_grid ? 1 : 0; + + if (start_row < 0) + start_row = 0; + if (end_row > eti->rows) + end_row = eti->rows; + + if (eti->uniform_row_height) { + return ((end_row - start_row) * (ETI_ROW_HEIGHT (eti, -1) + height_extra)); + } else { + gint row, total; + total = 0; + for (row = start_row; row < end_row; row++) + total += ETI_ROW_HEIGHT (eti, row) + height_extra; + + return total; + } +} + +static void +eti_get_region (ETableItem *eti, + gint start_col, + gint start_row, + gint end_col, + gint end_row, + gint *x1p, + gint *y1p, + gint *x2p, + gint *y2p) +{ + gint x1, y1, x2, y2; + + x1 = e_table_header_col_diff (eti->header, 0, start_col); + y1 = e_table_item_row_diff (eti, 0, start_row); + x2 = x1 + e_table_header_col_diff (eti->header, start_col, end_col + 1); + y2 = y1 + e_table_item_row_diff (eti, start_row, end_row + 1); + if (x1p) + *x1p = x1; + if (y1p) + *y1p = y1; + if (x2p) + *x2p = x2; + if (y2p) + *y2p = y2; +} + +/* + * eti_request_region_redraw: + * + * Request a canvas redraw on the range (start_col, start_row) to (end_col, end_row). + * This is inclusive (ie, you can use: 0,0-0,0 to redraw the first cell). + * + * The @border argument is a number of pixels around the region that should also be queued + * for redraw. This is typically used by the focus routines to queue a redraw for the + * border as well. + */ +static void +eti_request_region_redraw (ETableItem *eti, + gint start_col, + gint start_row, + gint end_col, + gint end_row, + gint border) +{ + gint x1, y1, x2, y2; + + if (eti->rows > 0) { + + eti_get_region ( + eti, + start_col, start_row, + end_col, end_row, + &x1, &y1, &x2, &y2); + + eti_item_region_redraw ( + eti, + x1 - border, + y1 - border, + x2 + 1 + border, + y2 + 1 + border); + } +} + +/* + * eti_request_region_show + * + * Request a canvas show on the range (start_col, start_row) to (end_col, end_row). + * This is inclusive (ie, you can use: 0,0-0,0 to show the first cell). + */ +static void +eti_request_region_show (ETableItem *eti, + gint start_col, + gint start_row, + gint end_col, + gint end_row, + gint delay) +{ + gint x1, y1, x2, y2; + + eti_get_region ( + eti, + start_col, start_row, + end_col, end_row, + &x1, &y1, &x2, &y2); + + if (delay) + e_canvas_item_show_area_delayed ( + GNOME_CANVAS_ITEM (eti), x1, y1, x2, y2, delay); + else + e_canvas_item_show_area ( + GNOME_CANVAS_ITEM (eti), x1, y1, x2, y2); +} + +static void +eti_show_cursor (ETableItem *eti, + gint delay) +{ + GnomeCanvasItem *item; + gint cursor_row; + + item = GNOME_CANVAS_ITEM (eti); + + if (!((item->flags & GNOME_CANVAS_ITEM_REALIZED) && eti->cell_views_realized)) + return; + + if (eti->frozen_count > 0) { + eti->queue_show_cursor = TRUE; + return; + } + +#if 0 + g_object_get ( + eti->selection, + "cursor_row", &cursor_row, + NULL); +#else + cursor_row = e_selection_model_cursor_row (eti->selection); +#endif + + d (g_print ("%s: cursor row: %d\n", __FUNCTION__, cursor_row)); + + if (cursor_row != -1) { + cursor_row = model_to_view_row (eti, cursor_row); + eti_request_region_show ( + eti, + 0, cursor_row, eti->cols - 1, cursor_row, + delay); + } +} + +static void +eti_check_cursor_bounds (ETableItem *eti) +{ + GnomeCanvasItem *item; + gint x1, y1, x2, y2; + gint cursor_row; + + item = GNOME_CANVAS_ITEM (eti); + + if (!((item->flags & GNOME_CANVAS_ITEM_REALIZED) && eti->cell_views_realized)) + return; + + if (eti->frozen_count > 0) { + return; + } + + g_object_get ( + eti->selection, + "cursor_row", &cursor_row, + NULL); + + if (cursor_row == -1) { + eti->cursor_x1 = -1; + eti->cursor_y1 = -1; + eti->cursor_x2 = -1; + eti->cursor_y2 = -1; + eti->cursor_on_screen = TRUE; + return; + } + + d (g_print ("%s: model cursor row: %d\n", __FUNCTION__, cursor_row)); + + cursor_row = model_to_view_row (eti, cursor_row); + + d (g_print ("%s: cursor row: %d\n", __FUNCTION__, cursor_row)); + + eti_get_region ( + eti, + 0, cursor_row, eti->cols - 1, cursor_row, + &x1, &y1, &x2, &y2); + eti->cursor_x1 = x1; + eti->cursor_y1 = y1; + eti->cursor_x2 = x2; + eti->cursor_y2 = y2; + eti->cursor_on_screen = e_canvas_item_area_shown (GNOME_CANVAS_ITEM (eti), x1, y1, x2, y2); + + d (g_print ("%s: cursor on screen: %s\n", __FUNCTION__, eti->cursor_on_screen ? "TRUE" : "FALSE")); +} + +static void +eti_maybe_show_cursor (ETableItem *eti, + gint delay) +{ + d (g_print ("%s: cursor on screen: %s\n", __FUNCTION__, eti->cursor_on_screen ? "TRUE" : "FALSE")); + if (eti->cursor_on_screen) + eti_show_cursor (eti, delay); + eti_check_cursor_bounds (eti); +} + +static gboolean +eti_idle_show_cursor_cb (gpointer data) +{ + ETableItem *eti = data; + + if (eti->selection) { + eti_show_cursor (eti, 0); + eti_check_cursor_bounds (eti); + } + + eti->cursor_idle_id = 0; + g_object_unref (eti); + return FALSE; +} + +static void +eti_idle_maybe_show_cursor (ETableItem *eti) +{ + d (g_print ("%s: cursor on screen: %s\n", __FUNCTION__, eti->cursor_on_screen ? "TRUE" : "FALSE")); + if (eti->cursor_on_screen) { + g_object_ref (eti); + if (!eti->cursor_idle_id) + eti->cursor_idle_id = g_idle_add (eti_idle_show_cursor_cb, eti); + } +} + +static void +eti_cancel_drag_due_to_model_change (ETableItem *eti) +{ + if (eti->maybe_in_drag) { + eti->maybe_in_drag = FALSE; + if (!eti->maybe_did_something) + e_selection_model_do_something (E_SELECTION_MODEL (eti->selection), eti->drag_row, eti->drag_col, eti->drag_state); + } + if (eti->in_drag) { + eti->in_drag = FALSE; + } +} + +static void +eti_freeze (ETableItem *eti) +{ + eti->frozen_count++; + d (g_print ("%s: %d\n", __FUNCTION__, eti->frozen_count)); +} + +static void +eti_unfreeze (ETableItem *eti) +{ + if (eti->frozen_count <= 0) + return; + + eti->frozen_count--; + d (g_print ("%s: %d\n", __FUNCTION__, eti->frozen_count)); + if (eti->frozen_count == 0 && eti->queue_show_cursor) { + eti_show_cursor (eti, 0); + eti_check_cursor_bounds (eti); + eti->queue_show_cursor = FALSE; + } +} + +/* + * Callback routine: invoked before the ETableModel suffers a change + */ +static void +eti_table_model_pre_change (ETableModel *table_model, + ETableItem *eti) +{ + eti_cancel_drag_due_to_model_change (eti); + eti_check_cursor_bounds (eti); + if (eti_editing (eti)) + e_table_item_leave_edit_(eti); + eti->motion_row = -1; + eti->motion_col = -1; + eti_freeze (eti); +} + +/* + * Callback routine: invoked when the ETableModel has not suffered a change + */ +static void +eti_table_model_no_change (ETableModel *table_model, + ETableItem *eti) +{ + eti_unfreeze (eti); +} + +/* + * Callback routine: invoked when the ETableModel has suffered a change + */ + +static void +eti_table_model_changed (ETableModel *table_model, + ETableItem *eti) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) { + eti_unfreeze (eti); + return; + } + + eti->rows = e_table_model_row_count (eti->table_model); + + free_height_cache (eti); + + eti_unfreeze (eti); + + eti->needs_compute_height = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); + + eti_idle_maybe_show_cursor (eti); +} + +static void +eti_table_model_row_changed (ETableModel *table_model, + gint row, + ETableItem *eti) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) { + eti_unfreeze (eti); + return; + } + + if ((!eti->uniform_row_height) && eti->height_cache && eti->height_cache[row] != -1 && eti_row_height_real (eti, row) != eti->height_cache[row]) { + eti_table_model_changed (table_model, eti); + return; + } + + eti_unfreeze (eti); + + e_table_item_redraw_row (eti, row); +} + +static void +eti_table_model_cell_changed (ETableModel *table_model, + gint col, + gint row, + ETableItem *eti) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) { + eti_unfreeze (eti); + return; + } + + if ((!eti->uniform_row_height) && eti->height_cache && eti->height_cache[row] != -1 && eti_row_height_real (eti, row) != eti->height_cache[row]) { + eti_table_model_changed (table_model, eti); + return; + } + + eti_unfreeze (eti); + + e_table_item_redraw_row (eti, row); +} + +static void +eti_table_model_rows_inserted (ETableModel *table_model, + gint row, + gint count, + ETableItem *eti) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) { + eti_unfreeze (eti); + return; + } + eti->rows = e_table_model_row_count (eti->table_model); + + if (eti->height_cache) { + gint i; + eti->height_cache = g_renew (int, eti->height_cache, eti->rows); + memmove (eti->height_cache + row + count, eti->height_cache + row, (eti->rows - count - row) * sizeof (gint)); + for (i = row; i < row + count; i++) + eti->height_cache[i] = -1; + } + + eti_unfreeze (eti); + + eti_idle_maybe_show_cursor (eti); + + eti->needs_compute_height = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); +} + +static void +eti_table_model_rows_deleted (ETableModel *table_model, + gint row, + gint count, + ETableItem *eti) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) { + eti_unfreeze (eti); + return; + } + + eti->rows = e_table_model_row_count (eti->table_model); + + if (eti->height_cache && (eti->rows > row)) { + memmove (eti->height_cache + row, eti->height_cache + row + count, (eti->rows - row) * sizeof (gint)); + } + + eti_unfreeze (eti); + + eti_idle_maybe_show_cursor (eti); + + eti->needs_compute_height = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); +} + +/** + * e_table_item_redraw_range + * @eti: %ETableItem which will be redrawn + * @start_col: The first col to redraw. + * @start_row: The first row to redraw. + * @end_col: The last col to redraw. + * @end_row: The last row to redraw. + * + * This routine redraws the given %ETableItem in the range given. The + * range is inclusive at both ends. + */ +void +e_table_item_redraw_range (ETableItem *eti, + gint start_col, + gint start_row, + gint end_col, + gint end_row) +{ + gint border; + gint cursor_col, cursor_row; + + g_return_if_fail (eti != NULL); + g_return_if_fail (E_IS_TABLE_ITEM (eti)); + + g_object_get ( + eti->selection, + "cursor_col", &cursor_col, + "cursor_row", &cursor_row, + NULL); + + if ((start_col == cursor_col) || + (end_col == cursor_col) || + (view_to_model_row (eti, start_row) == cursor_row) || + (view_to_model_row (eti, end_row) == cursor_row)) + border = 2; + else + border = 0; + + eti_request_region_redraw (eti, start_col, start_row, end_col, end_row, border); +} + +static void +e_table_item_redraw_row (ETableItem *eti, + gint row) +{ + if (row != -1) + e_table_item_redraw_range (eti, 0, row, eti->cols - 1, row); +} + +static void +eti_add_table_model (ETableItem *eti, + ETableModel *table_model) +{ + g_return_if_fail (eti->table_model == NULL); + + eti->table_model = table_model; + g_object_ref (eti->table_model); + + eti->table_model_pre_change_id = g_signal_connect ( + table_model, "model_pre_change", + G_CALLBACK (eti_table_model_pre_change), eti); + + eti->table_model_no_change_id = g_signal_connect ( + table_model, "model_no_change", + G_CALLBACK (eti_table_model_no_change), eti); + + eti->table_model_change_id = g_signal_connect ( + table_model, "model_changed", + G_CALLBACK (eti_table_model_changed), eti); + + eti->table_model_row_change_id = g_signal_connect ( + table_model, "model_row_changed", + G_CALLBACK (eti_table_model_row_changed), eti); + + eti->table_model_cell_change_id = g_signal_connect ( + table_model, "model_cell_changed", + G_CALLBACK (eti_table_model_cell_changed), eti); + + eti->table_model_rows_inserted_id = g_signal_connect ( + table_model, "model_rows_inserted", + G_CALLBACK (eti_table_model_rows_inserted), eti); + + eti->table_model_rows_deleted_id = g_signal_connect ( + table_model, "model_rows_deleted", + G_CALLBACK (eti_table_model_rows_deleted), eti); + + if (eti->header) { + eti_detach_cell_views (eti); + eti_attach_cell_views (eti); + } + + if (E_IS_TABLE_SUBSET (table_model)) { + eti->uses_source_model = 1; + eti->source_model = E_TABLE_SUBSET (table_model)->source; + if (eti->source_model) + g_object_ref (eti->source_model); + } + + eti_freeze (eti); + + eti_table_model_changed (table_model, eti); +} + +static void +eti_add_selection_model (ETableItem *eti, + ESelectionModel *selection) +{ + g_return_if_fail (eti->selection == NULL); + + eti->selection = selection; + g_object_ref (eti->selection); + + eti->selection_change_id = g_signal_connect ( + selection, "selection_changed", + G_CALLBACK (eti_selection_change), eti); + + eti->selection_row_change_id = g_signal_connect ( + selection, "selection_row_changed", + G_CALLBACK (eti_selection_row_change), eti); + + eti->cursor_change_id = g_signal_connect ( + selection, "cursor_changed", + G_CALLBACK (eti_cursor_change), eti); + + eti->cursor_activated_id = g_signal_connect ( + selection, "cursor_activated", + G_CALLBACK (eti_cursor_activated), eti); + + eti_selection_change (selection, eti); + g_signal_emit_by_name (eti, "selection_model_added", eti->selection); +} + +static void +eti_header_dim_changed (ETableHeader *eth, + gint col, + ETableItem *eti) +{ + eti->needs_compute_width = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); +} + +static void +eti_header_structure_changed (ETableHeader *eth, + ETableItem *eti) +{ + eti->cols = e_table_header_count (eti->header); + + /* + * There should be at least one column + * BUT: then you can't remove all columns from a header and add new ones. + */ + + if (eti->cell_views) { + eti_unrealize_cell_views (eti); + eti_detach_cell_views (eti); + eti_attach_cell_views (eti); + eti_realize_cell_views (eti); + } else { + if (eti->table_model) { + eti_attach_cell_views (eti); + eti_realize_cell_views (eti); + } + } + eti->needs_compute_width = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); +} + +static gint +eti_request_column_width (ETableHeader *eth, + gint col, + ETableItem *eti) +{ + gint width = 0; + + if (eti->cell_views && eti->cell_views_realized) { + width = e_cell_max_width (eti->cell_views[col], view_to_model_col (eti, col), col); + } + + return width; +} + +static void +eti_add_header_model (ETableItem *eti, + ETableHeader *header) +{ + g_return_if_fail (eti->header == NULL); + + eti->header = header; + g_object_ref (header); + + eti_header_structure_changed (header, eti); + + eti->header_dim_change_id = g_signal_connect ( + header, "dimension_change", + G_CALLBACK (eti_header_dim_changed), eti); + + eti->header_structure_change_id = g_signal_connect ( + header, "structure_change", + G_CALLBACK (eti_header_structure_changed), eti); + + eti->header_request_width_id = g_signal_connect ( + header, "request_width", + G_CALLBACK (eti_request_column_width), eti); +} + +/* + * GObject::dispose method + */ +static void +eti_dispose (GObject *object) +{ + ETableItem *eti = E_TABLE_ITEM (object); + + eti_remove_header_model (eti); + eti_remove_table_model (eti); + eti_remove_selection_model (eti); + + if (eti->height_cache_idle_id) { + g_source_remove (eti->height_cache_idle_id); + eti->height_cache_idle_id = 0; + } + eti->height_cache_idle_count = 0; + + if (eti->cursor_idle_id) { + g_source_remove (eti->cursor_idle_id); + eti->cursor_idle_id = 0; + } + + if (eti->height_cache) + g_free (eti->height_cache); + eti->height_cache = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (eti_parent_class)->dispose (object); +} + +static void +eti_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (object); + ETableItem *eti = E_TABLE_ITEM (object); + gint cursor_col; + + switch (property_id) { + case PROP_TABLE_HEADER: + eti_remove_header_model (eti); + eti_add_header_model (eti, E_TABLE_HEADER (g_value_get_object (value))); + break; + + case PROP_TABLE_MODEL: + eti_remove_table_model (eti); + eti_add_table_model (eti, E_TABLE_MODEL (g_value_get_object (value))); + break; + + case PROP_SELECTION_MODEL: + g_signal_emit_by_name ( + eti, "selection_model_removed", eti->selection); + eti_remove_selection_model (eti); + if (g_value_get_object (value)) + eti_add_selection_model (eti, E_SELECTION_MODEL (g_value_get_object (value))); + break; + + case PROP_LENGTH_THRESHOLD: + eti->length_threshold = g_value_get_int (value); + break; + + case PROP_TABLE_ALTERNATING_ROW_COLORS: + eti->alternating_row_colors = g_value_get_boolean (value); + break; + + case PROP_TABLE_HORIZONTAL_DRAW_GRID: + eti->horizontal_draw_grid = g_value_get_boolean (value); + break; + + case PROP_TABLE_VERTICAL_DRAW_GRID: + eti->vertical_draw_grid = g_value_get_boolean (value); + break; + + case PROP_TABLE_DRAW_FOCUS: + eti->draw_focus = g_value_get_boolean (value); + break; + + case PROP_CURSOR_MODE: + eti->cursor_mode = g_value_get_int (value); + break; + + case PROP_MINIMUM_WIDTH: + case PROP_WIDTH: + if ((eti->minimum_width == eti->width && g_value_get_double (value) > eti->width) || + g_value_get_double (value) < eti->width) { + eti->needs_compute_width = 1; + e_canvas_item_request_reflow (item); + } + eti->minimum_width = g_value_get_double (value); + break; + case PROP_CURSOR_ROW: + g_object_get ( + eti->selection, + "cursor_col", &cursor_col, + NULL); + + e_table_item_focus (eti, cursor_col != -1 ? cursor_col : 0, view_to_model_row (eti, g_value_get_int (value)), 0); + break; + case PROP_UNIFORM_ROW_HEIGHT: + if (eti->uniform_row_height != g_value_get_boolean (value)) { + eti->uniform_row_height = g_value_get_boolean (value); + if (item->flags & GNOME_CANVAS_ITEM_REALIZED) { + free_height_cache (eti); + eti->needs_compute_height = 1; + e_canvas_item_request_reflow (item); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (item); + } + } + break; + } + eti->needs_redraw = 1; + gnome_canvas_item_request_update (item); +} + +static void +eti_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableItem *eti; + gint row; + + eti = E_TABLE_ITEM (object); + + switch (property_id) { + case PROP_WIDTH: + g_value_set_double (value, eti->width); + break; + case PROP_HEIGHT: + g_value_set_double (value, eti->height); + break; + case PROP_MINIMUM_WIDTH: + g_value_set_double (value, eti->minimum_width); + break; + case PROP_CURSOR_ROW: + g_object_get ( + eti->selection, + "cursor_row", &row, + NULL); + g_value_set_int (value, model_to_view_row (eti, row)); + break; + case PROP_UNIFORM_ROW_HEIGHT: + g_value_set_boolean (value, eti->uniform_row_height); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +eti_init (ETableItem *eti) +{ + eti->motion_row = -1; + eti->motion_col = -1; + eti->editing_col = -1; + eti->editing_row = -1; + eti->height = 0; + eti->width = 0; + eti->minimum_width = 0; + + eti->save_col = -1; + eti->save_row = -1; + eti->save_state = NULL; + + eti->click_count = 0; + + eti->height_cache = NULL; + eti->height_cache_idle_id = 0; + eti->height_cache_idle_count = 0; + + eti->length_threshold = -1; + eti->uniform_row_height = FALSE; + + eti->uses_source_model = 0; + eti->source_model = NULL; + + eti->row_guess = -1; + eti->cursor_mode = E_CURSOR_SIMPLE; + + eti->selection_change_id = 0; + eti->selection_row_change_id = 0; + eti->cursor_change_id = 0; + eti->cursor_activated_id = 0; + eti->selection = NULL; + + eti->old_cursor_row = -1; + + eti->needs_redraw = 0; + eti->needs_compute_height = 0; + + eti->in_key_press = 0; + + eti->maybe_did_something = TRUE; + + eti->grabbed_count = 0; + eti->gtk_grabbed = 0; + + eti->in_drag = 0; + eti->maybe_in_drag = 0; + eti->grabbed = 0; + + eti->grabbed_col = -1; + eti->grabbed_row = -1; + + eti->cursor_on_screen = FALSE; + eti->cursor_x1 = -1; + eti->cursor_y1 = -1; + eti->cursor_x2 = -1; + eti->cursor_y2 = -1; + + eti->rows = -1; + eti->cols = -1; + + eti->frozen_count = 0; + eti->queue_show_cursor = FALSE; + + e_canvas_item_set_reflow_callback (GNOME_CANVAS_ITEM (eti), eti_reflow); +} + +#define gray50_width 2 +#define gray50_height 2 +static const gchar gray50_bits[] = { + 0x02, 0x01, }; + +static gboolean +eti_tree_unfreeze (GtkWidget *widget, + GdkEvent *event, + ETableItem *eti) +{ + + if (widget) + g_object_set_data (G_OBJECT (widget), "freeze-cursor", NULL); + + return FALSE; +} + +static void +eti_realize (GnomeCanvasItem *item) +{ + ETableItem *eti = E_TABLE_ITEM (item); + + if (GNOME_CANVAS_ITEM_CLASS (eti_parent_class)->realize) + (*GNOME_CANVAS_ITEM_CLASS (eti_parent_class)->realize)(item); + + eti->rows = e_table_model_row_count (eti->table_model); + + g_signal_connect ( + item->canvas, "scroll_event", + G_CALLBACK (eti_tree_unfreeze), eti); + + if (eti->cell_views == NULL) + eti_attach_cell_views (eti); + + eti_realize_cell_views (eti); + + free_height_cache (eti); + + if (item->canvas->focused_item == NULL && eti->selection) { + gint row; + + row = e_selection_model_cursor_row (E_SELECTION_MODEL (eti->selection)); + row = model_to_view_row (eti, row); + if (row != -1) { + e_canvas_item_grab_focus (item, FALSE); + eti_show_cursor (eti, 0); + eti_check_cursor_bounds (eti); + } + } + + eti->needs_compute_height = 1; + eti->needs_compute_width = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); +} + +static void +eti_unrealize (GnomeCanvasItem *item) +{ + ETableItem *eti = E_TABLE_ITEM (item); + + if (eti->grabbed_count > 0) { + d (g_print ("%s: eti_ungrab\n", __FUNCTION__)); + eti_ungrab (eti, -1); + } + + if (eti_editing (eti)) + e_table_item_leave_edit_(eti); + + if (eti->height_cache_idle_id) { + g_source_remove (eti->height_cache_idle_id); + eti->height_cache_idle_id = 0; + } + + if (eti->height_cache) + g_free (eti->height_cache); + eti->height_cache = NULL; + eti->height_cache_idle_count = 0; + + eti_unrealize_cell_views (eti); + + eti->height = 0; + + if (GNOME_CANVAS_ITEM_CLASS (eti_parent_class)->unrealize) + (*GNOME_CANVAS_ITEM_CLASS (eti_parent_class)->unrealize)(item); +} + +static void +eti_draw_grid_line (ETableItem *eti, + cairo_t *cr, + GtkStyle *style, + gint x1, + gint y1, + gint x2, + gint y2) +{ + cairo_save (cr); + + cairo_set_line_width (cr, 1.0); + gdk_cairo_set_source_color (cr, &style->dark[GTK_STATE_NORMAL]); + + cairo_move_to (cr, x1 + 0.5, y1 + 0.5); + cairo_line_to (cr, x2 + 0.5, y2 + 0.5); + cairo_stroke (cr); + + cairo_restore (cr); +} + +static void +eti_draw (GnomeCanvasItem *item, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height) +{ + ETableItem *eti = E_TABLE_ITEM (item); + const gint rows = eti->rows; + const gint cols = eti->cols; + gint row, col; + gint first_col, last_col, x_offset; + gint first_row, last_row, y_offset, yd; + gint x1, x2; + gint f_x1, f_x2, f_y1, f_y2; + gboolean f_found; + cairo_matrix_t i2c; + gdouble eti_base_x, eti_base_y, lower_right_y, lower_right_x; + GtkWidget *canvas = GTK_WIDGET (item->canvas); + GtkStyle *style = gtk_widget_get_style (canvas); + gint height_extra = eti->horizontal_draw_grid ? 1 : 0; + + /* + * Find out our real position after grouping + */ + gnome_canvas_item_i2c_matrix (item, &i2c); + eti_base_x = 0; + eti_base_y = 0; + cairo_matrix_transform_point (&i2c, &eti_base_x, &eti_base_y); + + lower_right_x = eti->width; + lower_right_y = eti->height; + cairo_matrix_transform_point (&i2c, &lower_right_x, &lower_right_y); + + /* + * First column to draw, last column to draw + */ + first_col = -1; + x_offset = 0; + x1 = floor (eti_base_x); + for (col = 0; col < cols; col++, x1 = x2) { + ETableCol *ecol = e_table_header_get_column (eti->header, col); + + x2 = x1 + ecol->width; + + if (x1 > (x + width)) + break; + if (x2 < x) + continue; + if (first_col == -1) { + x_offset = x1 - x; + first_col = col; + } + } + last_col = col; + + /* + * Nothing to paint + */ + if (first_col == -1) + return; + + /* + * Compute row span. + */ + if (eti->uniform_row_height) { + first_row = (y - floor (eti_base_y) - height_extra) / (ETI_ROW_HEIGHT (eti, -1) + height_extra); + last_row = (y + height - floor (eti_base_y) ) / (ETI_ROW_HEIGHT (eti, -1) + height_extra) + 1; + if (first_row > last_row) + return; + y_offset = floor (eti_base_y) - y + height_extra + first_row * (ETI_ROW_HEIGHT (eti, -1) + height_extra); + if (first_row < 0) + first_row = 0; + if (last_row > eti->rows) + last_row = eti->rows; + } else { + gint y1, y2; + + y_offset = 0; + first_row = -1; + + y1 = y2 = floor (eti_base_y) + height_extra; + for (row = 0; row < rows; row++, y1 = y2) { + + y2 += ETI_ROW_HEIGHT (eti, row) + height_extra; + + if (y1 > y + height) + break; + + if (y2 < y) + continue; + + if (first_row == -1) { + y_offset = y1 - y; + first_row = row; + } + } + last_row = row; + + if (first_row == -1) + return; + } + + if (first_row == -1) + return; + + /* + * Draw cells + */ + yd = y_offset; + f_x1 = f_x2 = f_y1 = f_y2 = -1; + f_found = FALSE; + + if (eti->horizontal_draw_grid && first_row == 0) + eti_draw_grid_line (eti, cr, style, eti_base_x - x, yd, eti_base_x + eti->width - x, yd); + + yd += height_extra; + + for (row = first_row; row < last_row; row++) { + gint xd; + gboolean selected; + gint cursor_col, cursor_row; + + height = ETI_ROW_HEIGHT (eti, row); + + xd = x_offset; + + selected = e_selection_model_is_row_selected (E_SELECTION_MODEL (eti->selection), view_to_model_row (eti,row)); + + g_object_get ( + eti->selection, + "cursor_col", &cursor_col, + "cursor_row", &cursor_row, + NULL); + + for (col = first_col; col < last_col; col++) { + ETableCol *ecol = e_table_header_get_column (eti->header, col); + ECellView *ecell_view = eti->cell_views[col]; + gboolean col_selected = selected; + gboolean cursor = FALSE; + ECellFlags flags; + gboolean free_background; + GdkColor *background; + gint x1, x2, y1, y2; + cairo_pattern_t *pat; + + switch (eti->cursor_mode) { + case E_CURSOR_SIMPLE: + case E_CURSOR_SPREADSHEET: + if (cursor_col == ecol->col_idx && cursor_row == view_to_model_row (eti, row)) { + col_selected = !col_selected; + cursor = TRUE; + } + break; + case E_CURSOR_LINE: + /* Nothing */ + break; + } + + x1 = xd; + y1 = yd + 1; + x2 = x1 + ecol->width; + y2 = yd + height; + + background = eti_get_cell_background_color (eti, row, col, col_selected, &free_background); + + cairo_save (cr); + pat = cairo_pattern_create_linear (0, y1, 0, y2); + cairo_pattern_add_color_stop_rgba ( + pat, 0.0, background->red / 65535.0 , + background->green / 65535.0, + background->blue / 65535.0, selected ? 0.8: 1.0); + if (selected) + cairo_pattern_add_color_stop_rgba ( + pat, 0.5, background->red / 65535.0 , + background->green / 65535.0, + background->blue / 65535.0, 0.9); + + cairo_pattern_add_color_stop_rgba ( + pat, 1, background->red / 65535.0 , + background->green / 65535.0, + background->blue / 65535.0, selected ? 0.8 : 1.0); + cairo_rectangle (cr, x1, y1, ecol->width, height - 1); + cairo_set_source (cr, pat); + cairo_fill_preserve (cr); + cairo_pattern_destroy (pat); + cairo_set_line_width (cr, 0); + cairo_stroke (cr); + cairo_restore (cr); + + cairo_save (cr); + cairo_set_line_width (cr, 1.0); + cairo_set_source_rgba ( + cr, background->red / 65535.0 , + background->green / 65535.0, + background->blue / 65535.0, 1); + cairo_move_to (cr, x1, y1); + cairo_line_to (cr, x2, y1); + cairo_stroke (cr); + + cairo_set_line_width (cr, 1.0); + cairo_set_source_rgba ( + cr, background->red / 65535.0 , + background->green / 65535.0, + background->blue / 65535.0, 1); + cairo_move_to (cr, x1, y2); + cairo_line_to (cr, x2, y2); + cairo_stroke (cr); + cairo_restore (cr); + + if (free_background) + gdk_color_free (background); + + flags = col_selected ? E_CELL_SELECTED : 0; + flags |= gtk_widget_has_focus (canvas) ? E_CELL_FOCUSED : 0; + flags |= cursor ? E_CELL_CURSOR : 0; + + switch (ecol->justification) { + case GTK_JUSTIFY_LEFT: + flags |= E_CELL_JUSTIFY_LEFT; + break; + case GTK_JUSTIFY_RIGHT: + flags |= E_CELL_JUSTIFY_RIGHT; + break; + case GTK_JUSTIFY_CENTER: + flags |= E_CELL_JUSTIFY_CENTER; + break; + case GTK_JUSTIFY_FILL: + flags |= E_CELL_JUSTIFY_FILL; + break; + } + + e_cell_draw ( + ecell_view, cr, ecol->col_idx, col, row, flags, + xd, yd, xd + ecol->width, yd + height); + + if (!f_found && !selected) { + switch (eti->cursor_mode) { + case E_CURSOR_LINE: + if (view_to_model_row (eti, row) == cursor_row) { + f_x1 = floor (eti_base_x) - x; + f_x2 = floor (lower_right_x) - x; + f_y1 = yd + 1; + f_y2 = yd + height; + f_found = TRUE; + } + break; + case E_CURSOR_SIMPLE: + case E_CURSOR_SPREADSHEET: + if (view_to_model_col (eti, col) == cursor_col && view_to_model_row (eti, row) == cursor_row) { + f_x1 = xd; + f_x2 = xd + ecol->width; + f_y1 = yd; + f_y2 = yd + height; + f_found = TRUE; + } + break; + } + } + + xd += ecol->width; + } + yd += height; + + if (eti->horizontal_draw_grid) { + eti_draw_grid_line (eti, cr, style, eti_base_x - x, yd, eti_base_x + eti->width - x, yd); + yd++; + } + } + + if (eti->vertical_draw_grid) { + gint xd = x_offset; + + for (col = first_col; col <= last_col; col++) { + ETableCol *ecol = e_table_header_get_column (eti->header, col); + + eti_draw_grid_line (eti, cr, style, xd, y_offset, xd, yd - 1); + + /* + * This looks wierd, but it is to draw the last line + */ + if (ecol) + xd += ecol->width; + } + } + + /* + * Draw focus + */ + if (eti->draw_focus && f_found) { + static const double dash[] = { 1.0, 1.0 }; + cairo_set_line_width (cr, 1.0); + cairo_rectangle ( + cr, + f_x1 + 0.5, f_x2 + 0.5, + f_x2 - f_x1 - 1, f_y2 - f_y1); + + gdk_cairo_set_source_color (cr, &style->bg[GTK_STATE_NORMAL]); + cairo_stroke_preserve (cr); + + cairo_set_dash (cr, dash, G_N_ELEMENTS (dash), 0.0); + gdk_cairo_set_source_color (cr, &style->fg[GTK_STATE_NORMAL]); + cairo_stroke (cr); + } +} + +static GnomeCanvasItem * +eti_point (GnomeCanvasItem *item, + gdouble x, + gdouble y, + gint cx, + gint cy) +{ + return item; +} + +static gboolean +find_cell (ETableItem *eti, + gdouble x, + gdouble y, + gint *view_col_res, + gint *view_row_res, + gdouble *x1_res, + gdouble *y1_res) +{ + const gint cols = eti->cols; + const gint rows = eti->rows; + gdouble x1, y1, x2, y2; + gint col, row; + + gint height_extra = eti->horizontal_draw_grid ? 1 : 0; + + /* FIXME: this routine is inneficient, fix later */ + + if (eti->grabbed_col >= 0 && eti->grabbed_row >= 0) { + *view_col_res = eti->grabbed_col; + *view_row_res = eti->grabbed_row; + *x1_res = x - e_table_header_col_diff (eti->header, 0, eti->grabbed_col); + *y1_res = y - e_table_item_row_diff (eti, 0, eti->grabbed_row); + return TRUE; + } + + if (cols == 0 || rows == 0) + return FALSE; + + x1 = 0; + for (col = 0; col < cols - 1; col++, x1 = x2) { + ETableCol *ecol = e_table_header_get_column (eti->header, col); + + if (x < x1) + return FALSE; + + x2 = x1 + ecol->width; + + if (x <= x2) + break; + } + + if (eti->uniform_row_height) { + if (y < height_extra) + return FALSE; + row = (y - height_extra) / (ETI_ROW_HEIGHT (eti, -1) + height_extra); + y1 = row * (ETI_ROW_HEIGHT (eti, -1) + height_extra) + height_extra; + if (row >= eti->rows) + return FALSE; + } else { + y1 = y2 = height_extra; + if (y < height_extra) + return FALSE; + for (row = 0; row < rows; row++, y1 = y2) { + y2 += ETI_ROW_HEIGHT (eti, row) + height_extra; + + if (y <= y2) + break; + } + + if (row == rows) + return FALSE; + } + *view_col_res = col; + if (x1_res) + *x1_res = x - x1; + *view_row_res = row; + if (y1_res) + *y1_res = y - y1; + return TRUE; +} + +static void +eti_cursor_move (ETableItem *eti, + gint row, + gint column) +{ + e_table_item_leave_edit_(eti); + e_table_item_focus (eti, view_to_model_col (eti, column), view_to_model_row (eti, row), 0); +} + +static void +eti_cursor_move_left (ETableItem *eti) +{ + gint cursor_col, cursor_row; + g_object_get ( + eti->selection, + "cursor_col", &cursor_col, + "cursor_row", &cursor_row, + NULL); + + eti_cursor_move (eti, model_to_view_row (eti, cursor_row), model_to_view_col (eti, cursor_col) - 1); +} + +static void +eti_cursor_move_right (ETableItem *eti) +{ + gint cursor_col, cursor_row; + g_object_get ( + eti->selection, + "cursor_col", &cursor_col, + "cursor_row", &cursor_row, + NULL); + + eti_cursor_move (eti, model_to_view_row (eti, cursor_row), model_to_view_col (eti, cursor_col) + 1); +} + +static gint +eti_e_cell_event (ETableItem *item, + ECellView *ecell_view, + GdkEvent *event, + gint model_col, + gint view_col, + gint row, + ECellFlags flags) +{ + ECellActions actions = 0; + gint ret_val; + + ret_val = e_cell_event ( + ecell_view, event, model_col, view_col, row, flags, &actions); + + if (actions & E_CELL_GRAB) { + GdkDevice *event_device; + guint32 event_time; + + d (g_print ("%s: eti_grab\n", __FUNCTION__)); + + event_device = gdk_event_get_device (event); + event_time = gdk_event_get_time (event); + eti_grab (item, event_device, event_time); + + item->grabbed_col = view_col; + item->grabbed_row = row; + } + + if (actions & E_CELL_UNGRAB) { + guint32 event_time; + + d (g_print ("%s: eti_ungrab\n", __FUNCTION__)); + + event_time = gdk_event_get_time (event); + eti_ungrab (item, event_time); + + item->grabbed_col = -1; + item->grabbed_row = -1; + } + + return ret_val; +} + +/* FIXME: cursor */ +static gint +eti_event (GnomeCanvasItem *item, + GdkEvent *event) +{ + ETableItem *eti = E_TABLE_ITEM (item); + ECellView *ecell_view; + GdkModifierType event_state = 0; + GdkEvent *event_copy; + guint event_button = 0; + guint event_keyval = 0; + gdouble event_x_item = 0; + gdouble event_y_item = 0; + gdouble event_x_win = 0; + gdouble event_y_win = 0; + guint32 event_time; + gboolean return_val = TRUE; +#if d(!)0 + gboolean leave = FALSE; +#endif + + if (!eti->header) + return FALSE; + + /* Don't fetch the device here. GnomeCanvas frequently emits + * synthesized events, and calling gdk_event_get_device() on them + * will trigger a runtime warning. Fetch the device where needed. */ + gdk_event_get_button (event, &event_button); + gdk_event_get_coords (event, &event_x_win, &event_y_win); + gdk_event_get_keyval (event, &event_keyval); + gdk_event_get_state (event, &event_state); + event_time = gdk_event_get_time (event); + + switch (event->type) { + case GDK_BUTTON_PRESS: { + gdouble x1, y1; + gint col, row; + gint cursor_row, cursor_col; + gint new_cursor_row, new_cursor_col; + ECellFlags flags = 0; + + d (g_print ("%s: GDK_BUTTON_PRESS received, button %d\n", __FUNCTION__, event_button)); + + switch (event_button) { + case 1: /* Fall through. */ + case 2: + e_canvas_item_grab_focus (GNOME_CANVAS_ITEM (eti), TRUE); + + event_x_item = event_x_win; + event_y_item = event_y_win; + + gnome_canvas_item_w2i ( + item, &event_x_item, &event_y_item); + + if (!find_cell (eti, event_x_item, event_y_item, &col, &row, &x1, &y1)) { + if (eti_editing (eti)) + e_table_item_leave_edit_(eti); + return TRUE; + } + + ecell_view = eti->cell_views[col]; + + /* Clone the event and alter its position. */ + event_copy = gdk_event_copy (event); + event_copy->button.x = x1; + event_copy->button.y = y1; + + g_object_get ( + eti->selection, + "cursor_row", &cursor_row, + "cursor_col", &cursor_col, + NULL); + + if (cursor_col == view_to_model_col (eti, col) && cursor_row == view_to_model_row (eti, row)) { + flags = E_CELL_CURSOR; + } else { + flags = 0; + } + + return_val = eti_e_cell_event ( + eti, ecell_view, event_copy, + view_to_model_col (eti, col), + col, row, flags); + if (return_val) { + gdk_event_free (event_copy); + return TRUE; + } + + g_signal_emit ( + eti, eti_signals[CLICK], 0, + row, view_to_model_col (eti, col), + event_copy, &return_val); + + gdk_event_free (event_copy); + + if (return_val) { + eti->click_count = 0; + return TRUE; + } + + g_object_get ( + eti->selection, + "cursor_row", &cursor_row, + "cursor_col", &cursor_col, + NULL); + + eti->maybe_did_something = + e_selection_model_maybe_do_something ( + E_SELECTION_MODEL (eti->selection), + view_to_model_row (eti, row), + view_to_model_col (eti, col), + event_state); + g_object_get ( + eti->selection, + "cursor_row", &new_cursor_row, + "cursor_col", &new_cursor_col, + NULL); + + if (cursor_row != new_cursor_row || cursor_col != new_cursor_col) { + eti->click_count = 1; + } else { + eti->click_count++; + eti->row_guess = row; + + if ((!eti_editing (eti)) && e_table_model_is_cell_editable (eti->table_model, cursor_col, row)) { + e_table_item_enter_edit (eti, col, row); + } + + /* + * Adjust the event positions + */ + + if (eti_editing (eti)) { + return_val = eti_e_cell_event ( + eti, ecell_view, event, + view_to_model_col (eti, col), + col, row, + E_CELL_EDITING | + E_CELL_CURSOR); + if (return_val) + return TRUE; + } + } + + if (event_button == 1) { + GdkDevice *event_device; + + return_val = TRUE; + + event_device = gdk_event_get_device (event); + + eti->maybe_in_drag = TRUE; + eti->drag_row = new_cursor_row; + eti->drag_col = new_cursor_col; + eti->drag_x = event_x_item; + eti->drag_y = event_y_item; + eti->drag_state = event_state; + eti->grabbed = TRUE; + d (g_print ("%s: eti_grab\n", __FUNCTION__)); + eti_grab (eti, event_device, event_time); + } + + break; + case 3: + e_canvas_item_grab_focus (GNOME_CANVAS_ITEM (eti), TRUE); + + event_x_item = event_x_win; + event_y_item = event_y_win; + + gnome_canvas_item_w2i ( + item, &event_x_item, &event_y_item); + + if (!find_cell (eti, event_x_item, event_y_item, &col, &row, &x1, &y1)) + return TRUE; + + e_selection_model_right_click_down ( + E_SELECTION_MODEL (eti->selection), + view_to_model_row (eti, row), + view_to_model_col (eti, col), 0); + + /* Clone the event and alter its position. */ + event_copy = gdk_event_copy (event); + event_copy->button.x = event_x_item; + event_copy->button.y = event_y_item; + + g_signal_emit ( + eti, eti_signals[RIGHT_CLICK], 0, + row, view_to_model_col (eti, col), + event, &return_val); + + gdk_event_free (event_copy); + + if (!return_val) + e_selection_model_right_click_up (E_SELECTION_MODEL (eti->selection)); + break; + case 4: + case 5: + return FALSE; + + } + break; + } + + case GDK_BUTTON_RELEASE: { + gdouble x1, y1; + gint col, row; + gint cursor_row, cursor_col; + + d (g_print ("%s: GDK_BUTTON_RELEASE received, button %d\n", __FUNCTION__, event_button)); + + if (eti->grabbed_count > 0) { + d (g_print ("%s: eti_ungrab\n", __FUNCTION__)); + eti_ungrab (eti, event_time); + } + + if (event_button == 1) { + if (eti->maybe_in_drag) { + eti->maybe_in_drag = FALSE; + if (!eti->maybe_did_something) + e_selection_model_do_something (E_SELECTION_MODEL (eti->selection), eti->drag_row, eti->drag_col, eti->drag_state); + } + if (eti->in_drag) { + eti->in_drag = FALSE; + } + } + + switch (event_button) { + case 1: /* Fall through. */ + case 2: + + event_x_item = event_x_win; + event_y_item = event_y_win; + + gnome_canvas_item_w2i ( + item, &event_x_item, &event_y_item); +#if d(!)0 + { + gboolean cell_found = find_cell ( + eti, event_x_item, event_y_item, + &col, &row, &x1, &y1); + g_print ( + "%s: find_cell(%f, %f) = %s(%d, %d, %f, %f)\n", + __FUNCTION__, event_x_item, event_y_item, + cell_found?"true":"false", col, row, x1, y1); + } +#endif + + if (!find_cell (eti, event_x_item, event_y_item, &col, &row, &x1, &y1)) + return TRUE; + + g_object_get ( + eti->selection, + "cursor_row", &cursor_row, + "cursor_col", &cursor_col, + NULL); + + if (eti_editing (eti) && cursor_row == view_to_model_row (eti, row) && cursor_col == view_to_model_col (eti, col)) { + + d (g_print ("%s: GDK_BUTTON_RELEASE received, button %d, line: %d\n", __FUNCTION__, event_button, __LINE__)) +; + + ecell_view = eti->cell_views[col]; + + /* Clone the event and alter its position. */ + event_copy = gdk_event_copy (event); + event_copy->button.x = x1; + event_copy->button.y = y1; + + return_val = eti_e_cell_event ( + eti, ecell_view, event_copy, + view_to_model_col (eti, col), + col, row, + E_CELL_EDITING | + E_CELL_CURSOR); + + gdk_event_free (event_copy); + } + break; + case 3: + e_selection_model_right_click_up (E_SELECTION_MODEL (eti->selection)); + return_val = TRUE; + break; + case 4: + case 5: + return FALSE; + + } + break; + } + + case GDK_2BUTTON_PRESS: { + gint model_col, model_row; +#if 0 + gdouble x1, y1; +#endif + + d (g_print ("%s: GDK_2BUTTON_PRESS received, button %d\n", __FUNCTION__, event_button)); + + /* + * click_count is so that if you click on two + * different rows we don't send a double click signal. + */ + + if (eti->click_count >= 2) { + + event_x_item = event_x_win; + event_y_item = event_y_win; + + gnome_canvas_item_w2i ( + item, &event_x_item, &event_y_item); + + g_object_get ( + eti->selection, + "cursor_row", &model_row, + "cursor_col", &model_col, + NULL); + + /* Clone the event and alter its position. */ + event_copy = gdk_event_copy (event); + event_copy->button.x = event_x_item - + e_table_header_col_diff ( + eti->header, 0, + model_to_view_col (eti, model_col)); + event_copy->button.y = event_y_item - + e_table_item_row_diff ( + eti, 0, + model_to_view_row (eti, model_row)); + + if (event_button == 1) { + if (eti->maybe_in_drag) { + eti->maybe_in_drag = FALSE; + if (!eti->maybe_did_something) + e_selection_model_do_something (E_SELECTION_MODEL (eti->selection), eti->drag_row, eti->drag_col, eti->drag_state); + } + if (eti->in_drag) { + eti->in_drag = FALSE; + } + if (eti_editing (eti)) + e_table_item_leave_edit_ (eti); + + } + + if (eti->grabbed_count > 0) { + d (g_print ("%s: eti_ungrab\n", __FUNCTION__)); + eti_ungrab (eti, event_time); + } + + if (model_row != -1 && model_col != -1) { + g_signal_emit ( + eti, eti_signals[DOUBLE_CLICK], 0, + model_row, model_col, event_copy); + } + + gdk_event_free (event_copy); + } + break; + } + case GDK_MOTION_NOTIFY: { + gint col, row, flags; + gdouble x1, y1; + gint cursor_col, cursor_row; + + event_x_item = event_x_win; + event_y_item = event_y_win; + + gnome_canvas_item_w2i (item, &event_x_item, &event_y_item); + + if (eti->maybe_in_drag) { + if (abs (event_x_item - eti->drag_x) >= 3 || + abs (event_y_item - eti->drag_y) >= 3) { + gboolean drag_handled; + + eti->maybe_in_drag = 0; + + /* Clone the event and + * alter its position. */ + event_copy = gdk_event_copy (event); + event_copy->motion.x = event_x_item; + event_copy->motion.y = event_y_item; + + g_signal_emit ( + eti, eti_signals[START_DRAG], 0, + eti->drag_row, eti->drag_col, + event_copy, &drag_handled); + + gdk_event_free (event_copy); + + if (drag_handled) + eti->in_drag = 1; + else + eti->in_drag = 0; + } + } + + if (!find_cell (eti, event_x_item, event_y_item, &col, &row, &x1, &y1)) + return TRUE; + + if (eti->motion_row != -1 && eti->motion_col != -1 && + (row != eti->motion_row || col != eti->motion_col)) { + GdkEvent *cross = gdk_event_new (GDK_LEAVE_NOTIFY); + cross->crossing.time = event_time; + return_val = eti_e_cell_event ( + eti, eti->cell_views[eti->motion_col], + cross, + view_to_model_col (eti, eti->motion_col), + eti->motion_col, eti->motion_row, 0); + } + + eti->motion_row = row; + eti->motion_col = col; + + g_object_get ( + eti->selection, + "cursor_row", &cursor_row, + "cursor_col", &cursor_col, + NULL); + + flags = 0; + if (cursor_row == view_to_model_row (eti, row) && cursor_col == view_to_model_col (eti, col)) { + flags = E_CELL_EDITING | E_CELL_CURSOR; + } + + ecell_view = eti->cell_views[col]; + + /* Clone the event and alter its position. */ + event_copy = gdk_event_copy (event); + event_copy->motion.x = x1; + event_copy->motion.y = y1; + + return_val = eti_e_cell_event ( + eti, ecell_view, event_copy, + view_to_model_col (eti, col), col, row, flags); + + gdk_event_free (event_copy); + + break; + } + + case GDK_KEY_PRESS: { + gint cursor_row, cursor_col; + gint handled = TRUE; + + d (g_print ("%s: GDK_KEY_PRESS received, keyval: %d\n", __FUNCTION__, (gint) e->key.keyval)); + + g_object_get ( + eti->selection, + "cursor_row", &cursor_row, + "cursor_col", &cursor_col, + NULL); + + if (cursor_row == -1 && cursor_col == -1) + return FALSE; + + eti->in_key_press = TRUE; + + switch (event_keyval) { + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (eti_editing (eti)) { + handled = FALSE; + break; + } + + g_signal_emit ( + eti, eti_signals[KEY_PRESS], 0, + model_to_view_row (eti, cursor_row), + cursor_col, event, &return_val); + if ((!return_val) && + (atk_get_root () || eti->cursor_mode != E_CURSOR_LINE) && + cursor_col != view_to_model_col (eti, 0)) + eti_cursor_move_left (eti); + return_val = 1; + break; + + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (eti_editing (eti)) { + handled = FALSE; + break; + } + + g_signal_emit ( + eti, eti_signals[KEY_PRESS], 0, + model_to_view_row (eti, cursor_row), + cursor_col, event, &return_val); + if ((!return_val) && + (atk_get_root () || eti->cursor_mode != E_CURSOR_LINE) && + cursor_col != view_to_model_col (eti, eti->cols - 1)) + eti_cursor_move_right (eti); + return_val = 1; + break; + + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + if ((event_state & GDK_MOD1_MASK) + && ((event_keyval == GDK_KEY_Down) || (event_keyval == GDK_KEY_KP_Down))) { + gint view_col = model_to_view_col (eti, cursor_col); + + if ((view_col >= 0) && (view_col < eti->cols)) + if (eti_e_cell_event (eti, eti->cell_views[view_col], event, cursor_col, view_col, model_to_view_row (eti, cursor_row), E_CELL_CURSOR)) + return TRUE; + } else + return_val = e_selection_model_key_press (E_SELECTION_MODEL (eti->selection), (GdkEventKey *) event); + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + if (eti_editing (eti)) { + handled = FALSE; + break; + } + + if (eti->cursor_mode != E_CURSOR_LINE) { + eti_cursor_move (eti, model_to_view_row (eti, cursor_row), 0); + return_val = TRUE; + } else + return_val = e_selection_model_key_press (E_SELECTION_MODEL (eti->selection), (GdkEventKey *) event); + break; + case GDK_KEY_End: + case GDK_KEY_KP_End: + if (eti_editing (eti)) { + handled = FALSE; + break; + } + + if (eti->cursor_mode != E_CURSOR_LINE) { + eti_cursor_move (eti, model_to_view_row (eti, cursor_row), eti->cols - 1); + return_val = TRUE; + } else + return_val = e_selection_model_key_press (E_SELECTION_MODEL (eti->selection), (GdkEventKey *) event); + break; + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + case GDK_KEY_ISO_Left_Tab: + if ((event_state & GDK_CONTROL_MASK) != 0) { + return_val = FALSE; + break; + } + if (eti->cursor_mode == E_CURSOR_SPREADSHEET) { + if ((event_state & GDK_SHIFT_MASK) != 0) { + /* shift tab */ + if (cursor_col != view_to_model_col (eti, 0)) + eti_cursor_move_left (eti); + else if (cursor_row != view_to_model_row (eti, 0)) + eti_cursor_move (eti, model_to_view_row (eti, cursor_row) - 1, eti->cols - 1); + else + return_val = FALSE; + } else { + if (cursor_col != view_to_model_col (eti, eti->cols - 1)) + eti_cursor_move_right (eti); + else if (cursor_row != view_to_model_row (eti, eti->rows - 1)) + eti_cursor_move (eti, model_to_view_row (eti, cursor_row) + 1, 0); + else + return_val = FALSE; + } + g_object_get ( + eti->selection, + "cursor_row", &cursor_row, + "cursor_col", &cursor_col, + NULL); + + if (cursor_col >= 0 && cursor_row >= 0 && return_val && + (!eti_editing (eti)) && e_table_model_is_cell_editable (eti->table_model, cursor_col, model_to_view_row (eti, cursor_row))) { + e_table_item_enter_edit (eti, model_to_view_col (eti, cursor_col), model_to_view_row (eti, cursor_row)); + } + break; + } else { + /* Let tab send you to the next widget. */ + return_val = FALSE; + break; + } + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + case GDK_KEY_ISO_Enter: + case GDK_KEY_3270_Enter: + if (eti_editing (eti)) { + ecell_view = eti->cell_views[eti->editing_col]; + return_val = eti_e_cell_event ( + eti, ecell_view, event, + view_to_model_col (eti, eti->editing_col), + eti->editing_col, eti->editing_row, E_CELL_EDITING | E_CELL_CURSOR | E_CELL_PREEDIT); + if (!return_val) + break; + } + g_signal_emit ( + eti, eti_signals[KEY_PRESS], 0, + model_to_view_row (eti, cursor_row), + cursor_col, event, &return_val); + if (!return_val) + return_val = e_selection_model_key_press (E_SELECTION_MODEL (eti->selection), (GdkEventKey *) event); + break; + + default: + handled = FALSE; + break; + } + + if (!handled) { + switch (event_keyval) { + case GDK_KEY_Scroll_Lock: + case GDK_KEY_Sys_Req: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Caps_Lock: + case GDK_KEY_Shift_Lock: + case GDK_KEY_Meta_L: + case GDK_KEY_Meta_R: + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Super_L: + case GDK_KEY_Super_R: + case GDK_KEY_Hyper_L: + case GDK_KEY_Hyper_R: + case GDK_KEY_ISO_Lock: + break; + + default: + if (!eti_editing (eti)) { + gint col, row; + row = model_to_view_row (eti, cursor_row); + col = model_to_view_col (eti, cursor_col); + if (col != -1 && row != -1 && e_table_model_is_cell_editable (eti->table_model, cursor_col, row)) { + e_table_item_enter_edit (eti, col, row); + } + } + if (!eti_editing (eti)) { + g_signal_emit ( + eti, eti_signals[KEY_PRESS], 0, + model_to_view_row (eti, cursor_row), + cursor_col, event, &return_val); + if (!return_val) + e_selection_model_key_press (E_SELECTION_MODEL (eti->selection), (GdkEventKey *) event); + } else { + ecell_view = eti->cell_views[eti->editing_col]; + return_val = eti_e_cell_event ( + eti, ecell_view, event, + view_to_model_col (eti, eti->editing_col), + eti->editing_col, eti->editing_row, E_CELL_EDITING | E_CELL_CURSOR); + if (!return_val) + e_selection_model_key_press (E_SELECTION_MODEL (eti->selection), (GdkEventKey *) event); + } + break; + } + } + eti->in_key_press = FALSE; + break; + } + + case GDK_KEY_RELEASE: { + gint cursor_row, cursor_col; + + d (g_print ("%s: GDK_KEY_RELEASE received, keyval: %d\n", __FUNCTION__, (gint) event_keyval)); + + g_object_get ( + eti->selection, + "cursor_row", &cursor_row, + "cursor_col", &cursor_col, + NULL); + + if (cursor_col == -1) + return FALSE; + + if (eti_editing (eti)) { + ecell_view = eti->cell_views[eti->editing_col]; + return_val = eti_e_cell_event ( + eti, ecell_view, event, + view_to_model_col (eti, eti->editing_col), + eti->editing_col, eti->editing_row, E_CELL_EDITING | E_CELL_CURSOR); + } + break; + } + + case GDK_LEAVE_NOTIFY: + d (leave = TRUE); + case GDK_ENTER_NOTIFY: + d (g_print ("%s: %s received\n", __FUNCTION__, leave ? "GDK_LEAVE_NOTIFY" : "GDK_ENTER_NOTIFY")); + if (eti->motion_row != -1 && eti->motion_col != -1) + return_val = eti_e_cell_event ( + eti, eti->cell_views[eti->motion_col], + event, + view_to_model_col (eti, eti->motion_col), + eti->motion_col, eti->motion_row, 0); + eti->motion_row = -1; + eti->motion_col = -1; + + break; + + case GDK_FOCUS_CHANGE: + d (g_print ("%s: GDK_FOCUS_CHANGE received, %s\n", __FUNCTION__, e->focus_change.in ? "in": "out")); + if (event->focus_change.in) { + if (eti->save_row != -1 && + eti->save_col != -1 && + !eti_editing (eti) && + e_table_model_is_cell_editable (eti->table_model, view_to_model_col (eti, eti->save_col), eti->save_row)) { + e_table_item_enter_edit (eti, eti->save_col, eti->save_row); + e_cell_load_state ( + eti->cell_views[eti->editing_col], view_to_model_col (eti, eti->save_col), + eti->save_col, eti->save_row, eti->edit_ctx, eti->save_state); + eti_free_save_state (eti); + } + } else { + if (eti_editing (eti)) { + eti_free_save_state (eti); + + eti->save_row = eti->editing_row; + eti->save_col = eti->editing_col; + eti->save_state = e_cell_save_state ( + eti->cell_views[eti->editing_col], view_to_model_col (eti, eti->editing_col), + eti->editing_col, eti->editing_row, eti->edit_ctx); + e_table_item_leave_edit_(eti); + } + } + + default: + return_val = FALSE; + } + /* d(g_print("%s: returning: %s\n", __FUNCTION__, return_val?"true":"false"));*/ + + return return_val; +} + +static void +eti_style_set (ETableItem *eti, + GtkStyle *previous_style) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) + return; + + if (eti->cell_views_realized) { + gint i; + gint n_cells = eti->n_cells; + + for (i = 0; i < n_cells; i++) { + e_cell_style_set (eti->cell_views[i], previous_style); + } + } + + eti->needs_compute_height = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (eti)); + eti->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); + + free_height_cache (eti); + + eti_idle_maybe_show_cursor (eti); +} + +static void +eti_class_init (ETableItemClass *class) +{ + GnomeCanvasItemClass *item_class = GNOME_CANVAS_ITEM_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = eti_dispose; + object_class->set_property = eti_set_property; + object_class->get_property = eti_get_property; + + item_class->update = eti_update; + item_class->realize = eti_realize; + item_class->unrealize = eti_unrealize; + item_class->draw = eti_draw; + item_class->point = eti_point; + item_class->event = eti_event; + + class->cursor_change = NULL; + class->cursor_activated = NULL; + class->double_click = NULL; + class->right_click = NULL; + class->click = NULL; + class->key_press = NULL; + class->start_drag = NULL; + class->style_set = eti_style_set; + class->selection_model_removed = NULL; + class->selection_model_added = NULL; + + g_object_class_install_property ( + object_class, + PROP_TABLE_HEADER, + g_param_spec_object ( + "ETableHeader", + "Table header", + "Table header", + E_TYPE_TABLE_HEADER, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_MODEL, + g_param_spec_object ( + "ETableModel", + "Table model", + "Table model", + E_TYPE_TABLE_MODEL, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_SELECTION_MODEL, + g_param_spec_object ( + "selection_model", + "Selection model", + "Selection model", + E_TYPE_SELECTION_MODEL, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_ALTERNATING_ROW_COLORS, + g_param_spec_boolean ( + "alternating_row_colors", + "Alternating Row Colors", + "Alternating Row Colors", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_HORIZONTAL_DRAW_GRID, + g_param_spec_boolean ( + "horizontal_draw_grid", + "Horizontal Draw Grid", + "Horizontal Draw Grid", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_VERTICAL_DRAW_GRID, + g_param_spec_boolean ( + "vertical_draw_grid", + "Vertical Draw Grid", + "Vertical Draw Grid", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_TABLE_DRAW_FOCUS, + g_param_spec_boolean ( + "drawfocus", + "Draw focus", + "Draw focus", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_MODE, + g_param_spec_int ( + "cursor_mode", + "Cursor mode", + "Cursor mode", + E_CURSOR_LINE, + E_CURSOR_SPREADSHEET, + E_CURSOR_LINE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_LENGTH_THRESHOLD, + g_param_spec_int ( + "length_threshold", + "Length Threshold", + "Length Threshold", + -1, G_MAXINT, 0, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_MINIMUM_WIDTH, + g_param_spec_double ( + "minimum_width", + "Minimum width", + "Minimum Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WIDTH, + g_param_spec_double ( + "width", + "Width", + "Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_HEIGHT, + g_param_spec_double ( + "height", + "Height", + "Height", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_ROW, + g_param_spec_int ( + "cursor_row", + "Cursor row", + "Cursor row", + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_UNIFORM_ROW_HEIGHT, + g_param_spec_boolean ( + "uniform_row_height", + "Uniform row height", + "Uniform row height", + FALSE, + G_PARAM_READWRITE)); + + eti_signals[CURSOR_CHANGE] = g_signal_new ( + "cursor_change", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableItemClass, cursor_change), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + eti_signals[CURSOR_ACTIVATED] = g_signal_new ( + "cursor_activated", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableItemClass, cursor_activated), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + eti_signals[DOUBLE_CLICK] = g_signal_new ( + "double_click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableItemClass, double_click), + NULL, NULL, + e_marshal_NONE__INT_INT_BOXED, + G_TYPE_NONE, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + eti_signals[START_DRAG] = g_signal_new ( + "start_drag", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableItemClass, start_drag), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + eti_signals[RIGHT_CLICK] = g_signal_new ( + "right_click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableItemClass, right_click), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + eti_signals[CLICK] = g_signal_new ( + "click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableItemClass, click), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + eti_signals[KEY_PRESS] = g_signal_new ( + "key_press", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableItemClass, key_press), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + eti_signals[STYLE_SET] = g_signal_new ( + "style_set", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableItemClass, style_set), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + GTK_TYPE_STYLE); + + eti_signals[SELECTION_MODEL_REMOVED] = g_signal_new ( + "selection_model_removed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (ETableItemClass, selection_model_removed), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + eti_signals[SELECTION_MODEL_ADDED] = g_signal_new ( + "selection_model_added", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (ETableItemClass, selection_model_added), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + /* A11y Init */ + gal_a11y_e_table_item_init (); +} + +/** + * e_table_item_set_cursor: + * @eti: %ETableItem which will have the cursor set. + * @col: Column to select. -1 means the last column. + * @row: Row to select. -1 means the last row. + * + * This routine sets the cursor of the %ETableItem canvas item. + */ +void +e_table_item_set_cursor (ETableItem *eti, + gint col, + gint row) +{ + e_table_item_focus (eti, col, view_to_model_row (eti, row), 0); +} + +static void +e_table_item_focus (ETableItem *eti, + gint col, + gint row, + GdkModifierType state) +{ + g_return_if_fail (eti != NULL); + g_return_if_fail (E_IS_TABLE_ITEM (eti)); + + if (row == -1) { + row = view_to_model_row (eti, eti->rows - 1); + } + + if (col == -1) { + col = eti->cols - 1; + } + + if (row != -1) { + e_selection_model_do_something ( + E_SELECTION_MODEL (eti->selection), + row, col, state); + } +} + +/** + * e_table_item_get_focused_column: + * @eti: %ETableItem which will have the cursor retrieved. + * + * This routine gets the cursor of the %ETableItem canvas item. + * + * Returns: The current cursor column. + */ +gint +e_table_item_get_focused_column (ETableItem *eti) +{ + gint cursor_col; + + g_return_val_if_fail (eti != NULL, -1); + g_return_val_if_fail (E_IS_TABLE_ITEM (eti), -1); + + g_object_get ( + eti->selection, + "cursor_col", &cursor_col, + NULL); + + return cursor_col; +} + +static void +eti_cursor_change (ESelectionModel *selection, + gint row, + gint col, + ETableItem *eti) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + gint view_row; + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) + return; + + view_row = model_to_view_row (eti, row); + + if (eti->old_cursor_row != -1 && view_row != eti->old_cursor_row) + e_table_item_redraw_row (eti, eti->old_cursor_row); + + if (view_row == -1) { + e_table_item_leave_edit_(eti); + eti->old_cursor_row = -1; + return; + } + + if (!e_table_model_has_change_pending (eti->table_model)) { + if (!eti->in_key_press) { + eti_maybe_show_cursor (eti, DOUBLE_CLICK_TIME + 10); + } else { + eti_maybe_show_cursor (eti, 0); + } + } + + e_canvas_item_grab_focus (GNOME_CANVAS_ITEM (eti), FALSE); + if (eti_editing (eti)) + e_table_item_leave_edit_(eti); + + g_signal_emit (eti, eti_signals[CURSOR_CHANGE], 0, view_row); + + e_table_item_redraw_row (eti, view_row); + + eti->old_cursor_row = view_row; +} + +static void +eti_cursor_activated (ESelectionModel *selection, + gint row, + gint col, + ETableItem *eti) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + gint view_row; + gint view_col; + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) + return; + + view_row = model_to_view_row (eti, row); + view_col = model_to_view_col (eti, col); + + if (view_row != -1 && view_col != -1) { + if (!e_table_model_has_change_pending (eti->table_model)) { + if (!eti->in_key_press) { + eti_show_cursor (eti, DOUBLE_CLICK_TIME + 10); + } else { + eti_show_cursor (eti, 0); + } + eti_check_cursor_bounds (eti); + } + } + + if (eti_editing (eti)) + e_table_item_leave_edit_(eti); + + if (view_row != -1) + g_signal_emit ( + eti, eti_signals[CURSOR_ACTIVATED], 0, view_row); +} + +static void +eti_selection_change (ESelectionModel *selection, + ETableItem *eti) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) + return; + + eti->needs_redraw = TRUE; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (eti)); +} + +static void +eti_selection_row_change (ESelectionModel *selection, + gint row, + ETableItem *eti) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (eti); + + if (!(item->flags & GNOME_CANVAS_ITEM_REALIZED)) + return; + + if (!eti->needs_redraw) { + e_table_item_redraw_row (eti, model_to_view_row (eti, row)); + } +} + +/** + * e_table_item_enter_edit + * @eti: %ETableItem which will start being edited + * @col: The view col to edit. + * @row: The view row to edit. + * + * This routine starts the given %ETableItem editing at the given view + * column and row. + */ +void +e_table_item_enter_edit (ETableItem *eti, + gint col, + gint row) +{ + g_return_if_fail (eti != NULL); + g_return_if_fail (E_IS_TABLE_ITEM (eti)); + + d (g_print ("%s: %d, %d, eti_editing() = %s\n", __FUNCTION__, col, row, eti_editing (eti)?"true":"false")); + + if (eti_editing (eti)) + e_table_item_leave_edit_(eti); + + eti->editing_col = col; + eti->editing_row = row; + + eti->edit_ctx = e_cell_enter_edit (eti->cell_views[col], view_to_model_col (eti, col), col, row); +} + +/** + * e_table_item_leave_edit_ + * @eti: %ETableItem which will stop being edited + * + * This routine stops the given %ETableItem from editing. + */ +void +e_table_item_leave_edit (ETableItem *eti) +{ + gint col, row; + gpointer edit_ctx; + + g_return_if_fail (eti != NULL); + g_return_if_fail (E_IS_TABLE_ITEM (eti)); + + d (g_print ("%s: eti_editing() = %s\n", __FUNCTION__, eti_editing (eti)?"true":"false")); + + if (!eti_editing (eti)) + return; + + col = eti->editing_col; + row = eti->editing_row; + edit_ctx = eti->edit_ctx; + + eti->editing_col = -1; + eti->editing_row = -1; + eti->edit_ctx = NULL; + + e_cell_leave_edit ( + eti->cell_views[col], + view_to_model_col (eti, col), + col, row, edit_ctx); +} + +/** + * e_table_item_compute_location + * @eti: %ETableItem to look in. + * @x: A pointer to the x location to find in the %ETableItem. + * @y: A pointer to the y location to find in the %ETableItem. + * @row: A pointer to the location to store the found row in. + * @col: A pointer to the location to store the found col in. + * + * This routine locates the pixel location (*x, *y) in the + * %ETableItem. If that location is in the %ETableItem, *row and *col + * are set to the view row and column where it was found. If that + * location is not in the %ETableItem, the height of the %ETableItem + * is removed from the value y points to. + */ +void +e_table_item_compute_location (ETableItem *eti, + gint *x, + gint *y, + gint *row, + gint *col) +{ + /* Save the grabbed row but make sure that we don't get flawed + * results because the cursor is grabbed. */ + gint grabbed_row = eti->grabbed_row; + eti->grabbed_row = -1; + + if (!find_cell (eti, *x, *y, col, row, NULL, NULL)) { + *y -= eti->height; + } + + eti->grabbed_row = grabbed_row; +} + +/** + * e_table_item_compute_mouse_over: + * Similar to e_table_item_compute_location, only here recalculating + * the position inside the item too. + **/ +void +e_table_item_compute_mouse_over (ETableItem *eti, + gint x, + gint y, + gint *row, + gint *col) +{ + gdouble realx, realy; + /* Save the grabbed row but make sure that we don't get flawed + * results because the cursor is grabbed. */ + gint grabbed_row = eti->grabbed_row; + eti->grabbed_row = -1; + + realx = x; + realy = y; + + gnome_canvas_item_w2i (GNOME_CANVAS_ITEM (eti), &realx, &realy); + + if (!find_cell (eti, (gint) realx, (gint) realy, col, row, NULL, NULL)) { + *row = -1; + *col = -1; + } + + eti->grabbed_row = grabbed_row; +} + +void +e_table_item_get_cell_geometry (ETableItem *eti, + gint *row, + gint *col, + gint *x, + gint *y, + gint *width, + gint *height) +{ + if (eti->rows > *row) { + if (x) + *x = e_table_header_col_diff (eti->header, 0, *col); + if (y) + *y = e_table_item_row_diff (eti, 0, *row); + if (width) + *width = e_table_header_col_diff (eti->header, *col, *col + 1); + if (height) + *height = ETI_ROW_HEIGHT (eti, *row); + *row = -1; + *col = -1; + } else { + *row -= eti->rows; + } +} + +typedef struct { + ETableItem *item; + gint rows_printed; +} ETableItemPrintContext; + +static gdouble * +e_table_item_calculate_print_widths (ETableHeader *eth, + gdouble width) +{ + gint i; + gdouble extra; + gdouble expansion; + gint last_resizable = -1; + gdouble scale = 1.0L; + gdouble *widths = g_new (gdouble, e_table_header_count (eth)); + /* - 1 to account for the last pixel border. */ + extra = width - 1; + expansion = 0; + for (i = 0; i < eth->col_count; i++) { + extra -= eth->columns[i]->min_width * scale; + if (eth->columns[i]->resizable && eth->columns[i]->expansion > 0) + last_resizable = i; + expansion += eth->columns[i]->resizable ? eth->columns[i]->expansion : 0; + widths[i] = eth->columns[i]->min_width * scale; + } + for (i = 0; i <= last_resizable; i++) { + widths[i] += extra * (eth->columns[i]->resizable ? eth->columns[i]->expansion : 0) / expansion; + } + + return widths; +} + +static gdouble +eti_printed_row_height (ETableItem *eti, + gdouble *widths, + GtkPrintContext *context, + gint row) +{ + gint col; + gint cols = eti->cols; + gdouble height = 0; + for (col = 0; col < cols; col++) { + ECellView *ecell_view = eti->cell_views[col]; + gdouble this_height = e_cell_print_height ( + ecell_view, context, view_to_model_col (eti, col), col, row, + widths[col] - 1); + if (this_height > height) + height = this_height; + } + return height; +} + +#define CHECK(x) if((x) == -1) return -1; + +static gint +gp_draw_rect (GtkPrintContext *context, + gdouble x, + gdouble y, + gdouble width, + gdouble height) +{ + cairo_t *cr; + cr = gtk_print_context_get_cairo_context (context); + cairo_save (cr); + cairo_rectangle (cr, x, y, width, height); + cairo_set_line_width (cr, 0.5); + cairo_stroke (cr); + cairo_restore (cr); + return 0; +} + +static void +e_table_item_print_page (EPrintable *ep, + GtkPrintContext *context, + gdouble width, + gdouble height, + gboolean quantize, + ETableItemPrintContext *itemcontext) +{ + ETableItem *eti = itemcontext->item; + const gint rows = eti->rows; + const gint cols = eti->cols; + gdouble max_height; + gint rows_printed = itemcontext->rows_printed; + gint row, col, next_page = 0; + gdouble yd = height; + cairo_t *cr; + gdouble *widths; + + cr = gtk_print_context_get_cairo_context (context); + max_height = gtk_print_context_get_height (context); + widths = e_table_item_calculate_print_widths (itemcontext->item->header, width); + + /* + * Draw cells + */ + + if (eti->horizontal_draw_grid) { + gp_draw_rect (context, 0, yd, width, 1); + } + yd++; + + for (row = rows_printed; row < rows; row++) { + gdouble xd = 1, row_height; + row_height = eti_printed_row_height (eti, widths, context, row); + + if (quantize) { + if (yd + row_height + 1 > max_height && row != rows_printed) { + next_page = 1; + break; + } + } else { + if (yd > max_height) { + next_page = 1; + break; + } + } + + for (col = 0; col < cols; col++) { + ECellView *ecell_view = eti->cell_views[col]; + + cairo_save (cr); + cairo_translate (cr, xd, yd); + cairo_rectangle (cr, 0, 0, widths[col] - 1, row_height); + cairo_clip (cr); + + e_cell_print ( + ecell_view, context, + view_to_model_col (eti, col), + col, + row, + widths[col] - 1, + row_height + 2); + + cairo_restore (cr); + + xd += widths[col]; + } + + yd += row_height; + if (eti->horizontal_draw_grid) { + gp_draw_rect (context, 0, yd, width, 1); + } + yd++; + } + + itemcontext->rows_printed = row; + if (eti->vertical_draw_grid) { + gdouble xd = 0; + for (col = 0; col < cols; col++) { + gp_draw_rect (context, xd, height, 1, yd - height); + xd += widths[col]; + } + gp_draw_rect (context, xd, height, 1, yd - height); + } + + if (next_page) + cairo_show_page (cr); + + g_free (widths); +} + +static gboolean +e_table_item_data_left (EPrintable *ep, + ETableItemPrintContext *itemcontext) +{ + ETableItem *item = itemcontext->item; + gint rows_printed = itemcontext->rows_printed; + + g_signal_stop_emission_by_name (ep, "data_left"); + return rows_printed < item->rows; +} + +static void +e_table_item_reset (EPrintable *ep, + ETableItemPrintContext *itemcontext) +{ + itemcontext->rows_printed = 0; +} + +static gdouble +e_table_item_height (EPrintable *ep, + GtkPrintContext *context, + gdouble width, + gdouble max_height, + gboolean quantize, + ETableItemPrintContext *itemcontext) +{ + ETableItem *item = itemcontext->item; + const gint rows = item->rows; + gint rows_printed = itemcontext->rows_printed; + gdouble *widths; + gint row; + gdouble yd = 0; + + widths = e_table_item_calculate_print_widths (itemcontext->item->header, width); + + /* + * Draw cells + */ + yd++; + + for (row = rows_printed; row < rows; row++) { + gdouble row_height; + + row_height = eti_printed_row_height (item, widths, context, row); + if (quantize) { + if (max_height != -1 && yd + row_height + 1 > max_height && row != rows_printed) { + break; + } + } else { + if (max_height != -1 && yd > max_height) { + break; + } + } + + yd += row_height; + + yd++; + } + + g_free (widths); + + if (max_height != -1 && (!quantize) && yd > max_height) + yd = max_height; + + g_signal_stop_emission_by_name (ep, "height"); + return yd; +} + +static gboolean +e_table_item_will_fit (EPrintable *ep, + GtkPrintContext *context, + gdouble width, + gdouble max_height, + gboolean quantize, + ETableItemPrintContext *itemcontext) +{ + ETableItem *item = itemcontext->item; + const gint rows = item->rows; + gint rows_printed = itemcontext->rows_printed; + gdouble *widths; + gint row; + gdouble yd = 0; + gboolean ret_val = TRUE; + + widths = e_table_item_calculate_print_widths (itemcontext->item->header, width); + + /* + * Draw cells + */ + yd++; + + for (row = rows_printed; row < rows; row++) { + gdouble row_height; + + row_height = eti_printed_row_height (item, widths, context, row); + if (quantize) { + if (max_height != -1 && yd + row_height + 1 > max_height && row != rows_printed) { + ret_val = FALSE; + break; + } + } else { + if (max_height != -1 && yd > max_height) { + ret_val = FALSE; + break; + } + } + + yd += row_height; + + yd++; + } + + g_free (widths); + + g_signal_stop_emission_by_name (ep, "will_fit"); + return ret_val; +} + +static void +e_table_item_printable_destroy (gpointer data, + GObject *where_object_was) +{ + ETableItemPrintContext *itemcontext = data; + + g_object_unref (itemcontext->item); + g_free (itemcontext); +} + +/** + * e_table_item_get_printable + * @eti: %ETableItem which will be printed + * + * This routine creates and returns an %EPrintable that can be used to + * print the given %ETableItem. + * + * Returns: The %EPrintable. + */ +EPrintable * +e_table_item_get_printable (ETableItem *item) +{ + EPrintable *printable = e_printable_new (); + ETableItemPrintContext *itemcontext; + + itemcontext = g_new (ETableItemPrintContext, 1); + itemcontext->item = item; + g_object_ref (item); + itemcontext->rows_printed = 0; + + g_signal_connect ( + printable, "print_page", + G_CALLBACK (e_table_item_print_page), itemcontext); + g_signal_connect ( + printable, "data_left", + G_CALLBACK (e_table_item_data_left), itemcontext); + g_signal_connect ( + printable, "reset", + G_CALLBACK (e_table_item_reset), itemcontext); + g_signal_connect ( + printable, "height", + G_CALLBACK (e_table_item_height), itemcontext); + g_signal_connect ( + printable, "will_fit", + G_CALLBACK (e_table_item_will_fit), itemcontext); + + g_object_weak_ref ( + G_OBJECT (printable), + e_table_item_printable_destroy, itemcontext); + + return printable; +} diff --git a/e-util/e-table-item.h b/e-util/e-table-item.h new file mode 100644 index 0000000000..09fdab90cc --- /dev/null +++ b/e-util/e-table-item.h @@ -0,0 +1,261 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@gnu.org> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_ITEM_H_ +#define _E_TABLE_ITEM_H_ + +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-printable.h> +#include <e-util/e-selection-model.h> +#include <e-util/e-table-defines.h> +#include <e-util/e-table-header.h> +#include <e-util/e-table-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_ITEM \ + (e_table_item_get_type ()) +#define E_TABLE_ITEM(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_ITEM, ETableItem)) +#define E_TABLE_ITEM_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_ITEM, ETableItemClass)) +#define E_IS_TABLE_ITEM(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_ITEM)) +#define E_IS_TABLE_ITEM_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_ITEM)) +#define E_TABLE_ITEM_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_ITEM, ETableItemClass)) + +G_BEGIN_DECLS + +typedef struct _ETableItem ETableItem; +typedef struct _ETableItemClass ETableItemClass; + +struct _ETableItem { + GnomeCanvasItem parent; + ETableModel *table_model; + ETableHeader *header; + + ETableModel *source_model; + ESelectionModel *selection; + + gint minimum_width, width, height; + + gint cols, rows; + + gint click_count; + + /* + * Ids for the signals we connect to + */ + gint header_dim_change_id; + gint header_structure_change_id; + gint header_request_width_id; + gint table_model_pre_change_id; + gint table_model_no_change_id; + gint table_model_change_id; + gint table_model_row_change_id; + gint table_model_cell_change_id; + gint table_model_rows_inserted_id; + gint table_model_rows_deleted_id; + + gint selection_change_id; + gint selection_row_change_id; + gint cursor_change_id; + gint cursor_activated_id; + + guint cursor_idle_id; + + /* View row, -1 means unknown */ + gint old_cursor_row; + + guint alternating_row_colors : 1; + guint horizontal_draw_grid : 1; + guint vertical_draw_grid : 1; + guint draw_focus : 1; + guint uniform_row_height : 1; + guint cell_views_realized : 1; + + guint needs_redraw : 1; + guint needs_compute_height : 1; + guint needs_compute_width : 1; + + guint uses_source_model : 1; + + guint in_key_press : 1; + + guint maybe_in_drag : 1; + guint in_drag : 1; + guint grabbed : 1; + + guint maybe_did_something : 1; + + guint cursor_on_screen : 1; + guint gtk_grabbed : 1; + + guint queue_show_cursor : 1; + guint grab_cancelled : 1; + + gint frozen_count; + + gint cursor_x1; + gint cursor_y1; + gint cursor_x2; + gint cursor_y2; + + gint drag_col; + gint drag_row; + gint drag_x; + gint drag_y; + guint drag_state; + + /* + * Realized views, per column + */ + ECellView **cell_views; + gint n_cells; + + gint *height_cache; + gint uniform_row_height_cache; + gint height_cache_idle_id; + gint height_cache_idle_count; + + /* + * Lengh Threshold: above this, we stop computing correctly + * the size + */ + gint length_threshold; + + gint row_guess; + ECursorMode cursor_mode; + + gint motion_col, motion_row; + + /* + * During editing + */ + gint editing_col, editing_row; + void *edit_ctx; + + gint save_col, save_row; + void *save_state; + + gint grabbed_col, grabbed_row; + gint grabbed_count; +}; + +struct _ETableItemClass { + GnomeCanvasItemClass parent_class; + + void (*cursor_change) (ETableItem *eti, + gint row); + void (*cursor_activated) (ETableItem *eti, + gint row); + void (*double_click) (ETableItem *eti, + gint row, + gint col, + GdkEvent *event); + gboolean (*right_click) (ETableItem *eti, + gint row, + gint col, + GdkEvent *event); + gboolean (*click) (ETableItem *eti, + gint row, + gint col, + GdkEvent *event); + gboolean (*key_press) (ETableItem *eti, + gint row, + gint col, + GdkEvent *event); + gboolean (*start_drag) (ETableItem *eti, + gint row, + gint col, + GdkEvent *event); + void (*style_set) (ETableItem *eti, + GtkStyle *previous_style); + void (*selection_model_removed) + (ETableItem *eti, + ESelectionModel *selection); + void (*selection_model_added) + (ETableItem *eti, + ESelectionModel *selection); +}; + +GType e_table_item_get_type (void) G_GNUC_CONST; + +/* + * Focus + */ +void e_table_item_set_cursor (ETableItem *eti, + gint col, + gint row); + +gint e_table_item_get_focused_column (ETableItem *eti); + +void e_table_item_leave_edit (ETableItem *eti); +void e_table_item_enter_edit (ETableItem *eti, + gint col, + gint row); + +void e_table_item_redraw_range (ETableItem *eti, + gint start_col, + gint start_row, + gint end_col, + gint end_row); + +EPrintable * e_table_item_get_printable (ETableItem *eti); +void e_table_item_compute_location (ETableItem *eti, + gint *x, + gint *y, + gint *row, + gint *col); +void e_table_item_compute_mouse_over (ETableItem *eti, + gint x, + gint y, + gint *row, + gint *col); +void e_table_item_get_cell_geometry (ETableItem *eti, + gint *row, + gint *col, + gint *x, + gint *y, + gint *width, + gint *height); + +gint e_table_item_row_diff (ETableItem *eti, + gint start_row, + gint end_row); + +G_END_DECLS + +#endif /* _E_TABLE_ITEM_H_ */ diff --git a/e-util/e-table-memory-callbacks.c b/e-util/e-table-memory-callbacks.c new file mode 100644 index 0000000000..a3f919b981 --- /dev/null +++ b/e-util/e-table-memory-callbacks.c @@ -0,0 +1,234 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-memory-callbacks.h" + +G_DEFINE_TYPE (ETableMemoryCallbacks, e_table_memory_callbacks, E_TYPE_TABLE_MEMORY) + +static gint +etmc_column_count (ETableModel *etm) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->col_count) + return etmc->col_count (etm, etmc->data); + else + return 0; +} + +static gpointer +etmc_value_at (ETableModel *etm, + gint col, + gint row) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->value_at) + return etmc->value_at (etm, col, row, etmc->data); + else + return NULL; +} + +static void +etmc_set_value_at (ETableModel *etm, + gint col, + gint row, + gconstpointer val) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->set_value_at) + etmc->set_value_at (etm, col, row, val, etmc->data); +} + +static gboolean +etmc_is_cell_editable (ETableModel *etm, + gint col, + gint row) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->is_cell_editable) + return etmc->is_cell_editable (etm, col, row, etmc->data); + else + return FALSE; +} + +/* The default for etmc_duplicate_value is to return the raw value. */ +static gpointer +etmc_duplicate_value (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->duplicate_value) + return etmc->duplicate_value (etm, col, value, etmc->data); + else + return (gpointer) value; +} + +static void +etmc_free_value (ETableModel *etm, + gint col, + gpointer value) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->free_value) + etmc->free_value (etm, col, value, etmc->data); +} + +static gpointer +etmc_initialize_value (ETableModel *etm, + gint col) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->initialize_value) + return etmc->initialize_value (etm, col, etmc->data); + else + return NULL; +} + +static gboolean +etmc_value_is_empty (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->value_is_empty) + return etmc->value_is_empty (etm, col, value, etmc->data); + else + return FALSE; +} + +static gchar * +etmc_value_to_string (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->value_to_string) + return etmc->value_to_string (etm, col, value, etmc->data); + else + return g_strdup (""); +} + +static void +etmc_append_row (ETableModel *etm, + ETableModel *source, + gint row) +{ + ETableMemoryCallbacks *etmc = E_TABLE_MEMORY_CALLBACKS (etm); + + if (etmc->append_row) + etmc->append_row (etm, source, row, etmc->data); +} + +static void +e_table_memory_callbacks_class_init (ETableMemoryCallbacksClass *class) +{ + ETableModelClass *model_class = E_TABLE_MODEL_CLASS (class); + + model_class->column_count = etmc_column_count; + model_class->value_at = etmc_value_at; + model_class->set_value_at = etmc_set_value_at; + model_class->is_cell_editable = etmc_is_cell_editable; + model_class->duplicate_value = etmc_duplicate_value; + model_class->free_value = etmc_free_value; + model_class->initialize_value = etmc_initialize_value; + model_class->value_is_empty = etmc_value_is_empty; + model_class->value_to_string = etmc_value_to_string; + model_class->append_row = etmc_append_row; + +} + +static void +e_table_memory_callbacks_init (ETableMemoryCallbacks *etmc) +{ + /* nothing to do */ +} + +/** + * e_table_memory_callbacks_new: + * @col_count: + * @value_at: + * @set_value_at: + * @is_cell_editable: + * @duplicate_value: + * @free_value: + * @initialize_value: + * @value_is_empty: + * @value_to_string: + * @data: closure pointer. + * + * This initializes a new ETableMemoryCallbacksModel object. + * ETableMemoryCallbacksModel is an implementaiton of the abstract class + * ETableModel. The ETableMemoryCallbacksModel is designed to allow people + * to easily create ETableModels without having to create a new GType + * derived from ETableModel every time they need one. + * + * Instead, ETableMemoryCallbacksModel uses a setup based in callback + * functions, every callback function signature mimics the signature of + * each ETableModel method and passes the extra @data pointer to each one + * of the method to provide them with any context they might want to use. + * + * Returns: An ETableMemoryCallbacksModel object (which is also an ETableModel + * object). + */ +ETableModel * +e_table_memory_callbacks_new (ETableMemoryCallbacksColumnCountFn col_count, + ETableMemoryCallbacksValueAtFn value_at, + ETableMemoryCallbacksSetValueAtFn set_value_at, + ETableMemoryCallbacksIsCellEditableFn is_cell_editable, + ETableMemoryCallbacksDuplicateValueFn duplicate_value, + ETableMemoryCallbacksFreeValueFn free_value, + ETableMemoryCallbacksInitializeValueFn initialize_value, + ETableMemoryCallbacksValueIsEmptyFn value_is_empty, + ETableMemoryCallbacksValueToStringFn value_to_string, + gpointer data) +{ + ETableMemoryCallbacks *et; + + et = g_object_new (E_TYPE_TABLE_MEMORY_CALLBACKS, NULL); + + et->col_count = col_count; + et->value_at = value_at; + et->set_value_at = set_value_at; + et->is_cell_editable = is_cell_editable; + et->duplicate_value = duplicate_value; + et->free_value = free_value; + et->initialize_value = initialize_value; + et->value_is_empty = value_is_empty; + et->value_to_string = value_to_string; + et->data = data; + + return (ETableModel *) et; + } diff --git a/e-util/e-table-memory-callbacks.h b/e-util/e-table-memory-callbacks.h new file mode 100644 index 0000000000..a71cac1d91 --- /dev/null +++ b/e-util/e-table-memory-callbacks.h @@ -0,0 +1,148 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_MEMORY_CALLBACKS_H_ +#define _E_TABLE_MEMORY_CALLBACKS_H_ + +#include <e-util/e-table-memory.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_MEMORY_CALLBACKS \ + (e_table_memory_callbacks_get_type ()) +#define E_TABLE_MEMORY_CALLBACKS(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_MEMORY_CALLBACKS, ETableMemoryCallbacks)) +#define E_TABLE_MEMORY_CALLBACKS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_MEMORY_CALLBACKS, ETableMemoryCallbacksClass)) +#define E_IS_TABLE_MEMORY_CALLBACKS(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_MEMORY_CALLBACKS)) +#define E_IS_TABLE_MEMORY_CALLBACKS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_MEMORY_CALLBACKS)) +#define E_TABLE_MEMORY_CALLBACKS_GET_CLASS(cls) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((cls), E_TYPE_TABLE_MEMORY_CALLBACKS, ETableMemoryCallbacksClass)) + +G_BEGIN_DECLS + +typedef struct _ETableMemoryCallbacks ETableMemoryCallbacks; +typedef struct _ETableMemoryCallbacksClass ETableMemoryCallbacksClass; + +typedef gint (*ETableMemoryCallbacksColumnCountFn) + (ETableModel *etm, + gpointer data); +typedef void (*ETableMemoryCallbacksAppendRowFn) + (ETableModel *etm, + ETableModel *model, + gint row, + gpointer data); + +typedef gpointer (*ETableMemoryCallbacksValueAtFn) + (ETableModel *etm, + gint col, + gint row, + gpointer data); +typedef void (*ETableMemoryCallbacksSetValueAtFn) + (ETableModel *etm, + gint col, + gint row, + gconstpointer val, + gpointer data); +typedef gboolean (*ETableMemoryCallbacksIsCellEditableFn) + (ETableModel *etm, + gint col, + gint row, + gpointer data); + +typedef gpointer (*ETableMemoryCallbacksDuplicateValueFn) + (ETableModel *etm, + gint col, + gconstpointer val, + gpointer data); +typedef void (*ETableMemoryCallbacksFreeValueFn) + (ETableModel *etm, + gint col, + gpointer val, + gpointer data); +typedef gpointer (*ETableMemoryCallbacksInitializeValueFn) + (ETableModel *etm, + gint col, + gpointer data); +typedef gboolean (*ETableMemoryCallbacksValueIsEmptyFn) + (ETableModel *etm, + gint col, + gconstpointer val, + gpointer data); +typedef gchar * (*ETableMemoryCallbacksValueToStringFn) + (ETableModel *etm, + gint col, + gconstpointer val, + gpointer data); + +struct _ETableMemoryCallbacks { + ETableMemory parent; + + ETableMemoryCallbacksColumnCountFn col_count; + ETableMemoryCallbacksAppendRowFn append_row; + + ETableMemoryCallbacksValueAtFn value_at; + ETableMemoryCallbacksSetValueAtFn set_value_at; + ETableMemoryCallbacksIsCellEditableFn is_cell_editable; + + ETableMemoryCallbacksDuplicateValueFn duplicate_value; + ETableMemoryCallbacksFreeValueFn free_value; + ETableMemoryCallbacksInitializeValueFn initialize_value; + ETableMemoryCallbacksValueIsEmptyFn value_is_empty; + ETableMemoryCallbacksValueToStringFn value_to_string; + gpointer data; +}; + +struct _ETableMemoryCallbacksClass { + ETableMemoryClass parent_class; +}; + +GType e_table_memory_callbacks_get_type + (void) G_GNUC_CONST; +ETableModel * e_table_memory_callbacks_new + (ETableMemoryCallbacksColumnCountFn col_count, + + ETableMemoryCallbacksValueAtFn value_at, + ETableMemoryCallbacksSetValueAtFn set_value_at, + ETableMemoryCallbacksIsCellEditableFn is_cell_editable, + + ETableMemoryCallbacksDuplicateValueFn duplicate_value, + ETableMemoryCallbacksFreeValueFn free_value, + ETableMemoryCallbacksInitializeValueFn initialize_value, + ETableMemoryCallbacksValueIsEmptyFn value_is_empty, + ETableMemoryCallbacksValueToStringFn value_to_string, + gpointer data); + +G_END_DECLS + +#endif /* _E_TABLE_MEMORY_CALLBACKS_H_ */ + diff --git a/e-util/e-table-memory-store.c b/e-util/e-table-memory-store.c new file mode 100644 index 0000000000..066d319122 --- /dev/null +++ b/e-util/e-table-memory-store.c @@ -0,0 +1,637 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include "e-table-memory-store.h" + +#define E_TABLE_MEMORY_STORE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TABLE_MEMORY_STORE, ETableMemoryStorePrivate)) + +#define STORE_LOCATOR(etms, col, row) (*((etms)->priv->store + (row) * (etms)->priv->col_count + (col))) + +struct _ETableMemoryStorePrivate { + gint col_count; + ETableMemoryStoreColumnInfo *columns; + gpointer *store; +}; + +G_DEFINE_TYPE (ETableMemoryStore, e_table_memory_store, E_TYPE_TABLE_MEMORY) + +static gpointer +duplicate_value (ETableMemoryStore *etms, + gint col, + gconstpointer val) +{ + switch (etms->priv->columns[col].type) { + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_STRING: + return g_strdup (val); + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_PIXBUF: + if (val) + g_object_ref ((gpointer) val); + return (gpointer) val; + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_OBJECT: + if (val) + g_object_ref ((gpointer) val); + return (gpointer) val; + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_CUSTOM: + if (etms->priv->columns[col].custom.duplicate_value) + return etms->priv->columns[col].custom.duplicate_value (E_TABLE_MODEL (etms), col, val, NULL); + break; + default: + break; + } + return (gpointer) val; +} + +static void +free_value (ETableMemoryStore *etms, + gint col, + gpointer value) +{ + switch (etms->priv->columns[col].type) { + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_STRING: + g_free (value); + break; + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_PIXBUF: + if (value) + g_object_unref (value); + break; + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_OBJECT: + if (value) + g_object_unref (value); + break; + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_CUSTOM: + if (etms->priv->columns[col].custom.free_value) + etms->priv->columns[col].custom.free_value (E_TABLE_MODEL (etms), col, value, NULL); + break; + default: + break; + } +} + +static gint +etms_column_count (ETableModel *etm) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + + return etms->priv->col_count; +} + +static gpointer +etms_value_at (ETableModel *etm, + gint col, + gint row) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + + return STORE_LOCATOR (etms, col, row); +} + +static void +etms_set_value_at (ETableModel *etm, + gint col, + gint row, + gconstpointer val) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + + e_table_model_pre_change (etm); + + STORE_LOCATOR (etms, col, row) = duplicate_value (etms, col, val); + + e_table_model_cell_changed (etm, col, row); +} + +static gboolean +etms_is_cell_editable (ETableModel *etm, + gint col, + gint row) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + + return etms->priv->columns[col].editable; +} + +/* The default for etms_duplicate_value is to return the raw value. */ +static gpointer +etms_duplicate_value (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + + return duplicate_value (etms, col, value); +} + +static void +etms_free_value (ETableModel *etm, + gint col, + gpointer value) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + + free_value (etms, col, value); +} + +static gpointer +etms_initialize_value (ETableModel *etm, + gint col) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + + switch (etms->priv->columns[col].type) { + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_STRING: + return g_strdup (""); + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_PIXBUF: + return NULL; + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_CUSTOM: + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_OBJECT: + if (etms->priv->columns[col].custom.initialize_value) + return etms->priv->columns[col].custom.initialize_value (E_TABLE_MODEL (etms), col, NULL); + break; + default: + break; + } + return NULL; +} + +static gboolean +etms_value_is_empty (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + + switch (etms->priv->columns[col].type) { + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_STRING: + return !(value && *(gchar *) value); + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_PIXBUF: + return value == NULL; + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_CUSTOM: + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_OBJECT: + if (etms->priv->columns[col].custom.value_is_empty) + return etms->priv->columns[col].custom.value_is_empty (E_TABLE_MODEL (etms), col, value, NULL); + break; + default: + break; + } + return value == NULL; +} + +static gchar * +etms_value_to_string (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + + switch (etms->priv->columns[col].type) { + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_STRING: + return g_strdup (value); + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_PIXBUF: + return g_strdup (""); + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_CUSTOM: + case E_TABLE_MEMORY_STORE_COLUMN_TYPE_OBJECT: + if (etms->priv->columns[col].custom.value_is_empty) + return etms->priv->columns[col].custom.value_to_string (E_TABLE_MODEL (etms), col, value, NULL); + break; + default: + break; + } + return g_strdup_printf ("%d", GPOINTER_TO_INT (value)); +} + +static void +etms_append_row (ETableModel *etm, + ETableModel *source, + gint row) +{ + ETableMemoryStore *etms = E_TABLE_MEMORY_STORE (etm); + gpointer *new_data; + gint i; + gint row_count; + + new_data = g_new (gpointer , etms->priv->col_count); + + for (i = 0; i < etms->priv->col_count; i++) { + new_data[i] = e_table_model_value_at (source, i, row); + } + + row_count = e_table_model_row_count (E_TABLE_MODEL (etms)); + + e_table_memory_store_insert_array (etms, row_count, new_data, NULL); +} + +static void +etms_finalize (GObject *object) +{ + ETableMemoryStorePrivate *priv; + + priv = E_TABLE_MEMORY_STORE_GET_PRIVATE (object); + + e_table_memory_store_clear (E_TABLE_MEMORY_STORE (object)); + + g_free (priv->columns); + g_free (priv->store); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_table_memory_store_parent_class)->finalize (object); +} + +static void +e_table_memory_store_init (ETableMemoryStore *etms) +{ + etms->priv = E_TABLE_MEMORY_STORE_GET_PRIVATE (etms); +} + +static void +e_table_memory_store_class_init (ETableMemoryStoreClass *class) +{ + GObjectClass *object_class; + ETableModelClass *model_class; + + g_type_class_add_private (class, sizeof (ETableMemoryStorePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = etms_finalize; + + model_class = E_TABLE_MODEL_CLASS (class); + model_class->column_count = etms_column_count; + model_class->value_at = etms_value_at; + model_class->set_value_at = etms_set_value_at; + model_class->is_cell_editable = etms_is_cell_editable; + model_class->duplicate_value = etms_duplicate_value; + model_class->free_value = etms_free_value; + model_class->initialize_value = etms_initialize_value; + model_class->value_is_empty = etms_value_is_empty; + model_class->value_to_string = etms_value_to_string; + model_class->append_row = etms_append_row; +} + +/** + * e_table_memory_store_new: + * @col_count: + * @value_at: + * @set_value_at: + * @is_cell_editable: + * @duplicate_value: + * @free_value: + * @initialize_value: + * @value_is_empty: + * @value_to_string: + * @data: closure pointer. + * + * This initializes a new ETableMemoryStoreModel object. ETableMemoryStoreModel is + * an implementaiton of the abstract class ETableModel. The ETableMemoryStoreModel + * is designed to allow people to easily create ETableModels without having + * to create a new GType derived from ETableModel every time they need one. + * + * Instead, ETableMemoryStoreModel uses a setup based in callback functions, every + * callback function signature mimics the signature of each ETableModel method + * and passes the extra @data pointer to each one of the method to provide them + * with any context they might want to use. + * + * Returns: An ETableMemoryStoreModel object (which is also an ETableModel + * object). + */ +ETableModel * +e_table_memory_store_new (ETableMemoryStoreColumnInfo *columns) +{ + ETableMemoryStore *et = g_object_new (E_TYPE_TABLE_MEMORY_STORE, NULL); + + if (e_table_memory_store_construct (et, columns)) { + return (ETableModel *) et; + } else { + g_object_unref (et); + return NULL; + } +} + +ETableModel * +e_table_memory_store_construct (ETableMemoryStore *etms, + ETableMemoryStoreColumnInfo *columns) +{ + gint i; + for (i = 0; columns[i].type != E_TABLE_MEMORY_STORE_COLUMN_TYPE_TERMINATOR; i++) + /* Intentionally blank */; + etms->priv->col_count = i; + + etms->priv->columns = g_new (ETableMemoryStoreColumnInfo, etms->priv->col_count + 1); + + memcpy (etms->priv->columns, columns, (etms->priv->col_count + 1) * sizeof (ETableMemoryStoreColumnInfo)); + + return E_TABLE_MODEL (etms); +} + +void +e_table_memory_store_adopt_value_at (ETableMemoryStore *etms, + gint col, + gint row, + gpointer value) +{ + e_table_model_pre_change (E_TABLE_MODEL (etms)); + + STORE_LOCATOR (etms, col, row) = value; + + e_table_model_cell_changed (E_TABLE_MODEL (etms), col, row); +} + +/* The size of these arrays is the number of columns. */ +void +e_table_memory_store_insert_array (ETableMemoryStore *etms, + gint row, + gpointer *store, + gpointer data) +{ + gint row_count; + gint i; + + row_count = e_table_model_row_count (E_TABLE_MODEL (etms)) + 1; + if (row == -1) + row = row_count - 1; + etms->priv->store = g_realloc (etms->priv->store, etms->priv->col_count * row_count * sizeof (gpointer)); + memmove ( + etms->priv->store + etms->priv->col_count * (row + 1), + etms->priv->store + etms->priv->col_count * row, + etms->priv->col_count * (row_count - row - 1) * sizeof (gpointer)); + + for (i = 0; i < etms->priv->col_count; i++) { + STORE_LOCATOR (etms, i, row) = duplicate_value (etms, i, store[i]); + } + + e_table_memory_insert (E_TABLE_MEMORY (etms), row, data); +} + +void +e_table_memory_store_insert (ETableMemoryStore *etms, + gint row, + gpointer data, + ...) +{ + gpointer *store; + va_list args; + gint i; + + store = g_new (gpointer , etms->priv->col_count + 1); + + va_start (args, data); + for (i = 0; i < etms->priv->col_count; i++) { + store[i] = va_arg (args, gpointer); + } + va_end (args); + + e_table_memory_store_insert_array (etms, row, store, data); + + g_free (store); +} + +void +e_table_memory_store_insert_adopt_array (ETableMemoryStore *etms, + gint row, + gpointer *store, + gpointer data) +{ + gint row_count; + gint i; + + row_count = e_table_model_row_count (E_TABLE_MODEL (etms)) + 1; + if (row == -1) + row = row_count - 1; + etms->priv->store = g_realloc (etms->priv->store, etms->priv->col_count * row_count * sizeof (gpointer)); + memmove ( + etms->priv->store + etms->priv->col_count * (row + 1), + etms->priv->store + etms->priv->col_count * row, + etms->priv->col_count * (row_count - row - 1) * sizeof (gpointer)); + + for (i = 0; i < etms->priv->col_count; i++) { + STORE_LOCATOR (etms, i, row) = store[i]; + } + + e_table_memory_insert (E_TABLE_MEMORY (etms), row, data); +} + +void +e_table_memory_store_insert_adopt (ETableMemoryStore *etms, + gint row, + gpointer data, + ...) +{ + gpointer *store; + va_list args; + gint i; + + store = g_new (gpointer , etms->priv->col_count + 1); + + va_start (args, data); + for (i = 0; i < etms->priv->col_count; i++) { + store[i] = va_arg (args, gpointer); + } + va_end (args); + + e_table_memory_store_insert_adopt_array (etms, row, store, data); + + g_free (store); +} + +/** + * e_table_memory_store_change_array: + * @etms: the ETabelMemoryStore. + * @row: the row we're changing. + * @store: an array of new values to fill the row + * @data: the new closure to associate with this row. + * + * frees existing values associated with a row and replaces them with + * duplicates of the values in store. + * + */ +void +e_table_memory_store_change_array (ETableMemoryStore *etms, + gint row, + gpointer *store, + gpointer data) +{ + gint i; + + g_return_if_fail (row >= 0 && row < e_table_model_row_count (E_TABLE_MODEL (etms))); + + e_table_model_pre_change (E_TABLE_MODEL (etms)); + + for (i = 0; i < etms->priv->col_count; i++) { + free_value (etms, i, STORE_LOCATOR (etms, i, row)); + STORE_LOCATOR (etms, i, row) = duplicate_value (etms, i, store[i]); + } + + e_table_memory_set_data (E_TABLE_MEMORY (etms), row, data); + e_table_model_row_changed (E_TABLE_MODEL (etms), row); +} + +/** + * e_table_memory_store_change: + * @etms: the ETabelMemoryStore. + * @row: the row we're changing. + * @data: the new closure to associate with this row. + * + * a varargs version of e_table_memory_store_change_array. you must + * pass in etms->col_count args. + */ +void +e_table_memory_store_change (ETableMemoryStore *etms, + gint row, + gpointer data, + ...) +{ + gpointer *store; + va_list args; + gint i; + + g_return_if_fail (row >= 0 && row < e_table_model_row_count (E_TABLE_MODEL (etms))); + + store = g_new0 (gpointer , etms->priv->col_count + 1); + + va_start (args, data); + for (i = 0; i < etms->priv->col_count; i++) { + store[i] = va_arg (args, gpointer); + } + va_end (args); + + e_table_memory_store_change_array (etms, row, store, data); + + g_free (store); +} + +/** + * e_table_memory_store_change_adopt_array: + * @etms: the ETableMemoryStore + * @row: the row we're changing. + * @store: an array of new values to fill the row + * @data: the new closure to associate with this row. + * + * frees existing values for the row and stores the values from store + * into it. This function differs from + * e_table_memory_storage_change_adopt_array in that it does not + * duplicate the data. + */ +void +e_table_memory_store_change_adopt_array (ETableMemoryStore *etms, + gint row, + gpointer *store, + gpointer data) +{ + gint i; + + g_return_if_fail (row >= 0 && row < e_table_model_row_count (E_TABLE_MODEL (etms))); + + for (i = 0; i < etms->priv->col_count; i++) { + free_value (etms, i, STORE_LOCATOR (etms, i, row)); + STORE_LOCATOR (etms, i, row) = store[i]; + } + + e_table_memory_set_data (E_TABLE_MEMORY (etms), row, data); + e_table_model_row_changed (E_TABLE_MODEL (etms), row); +} + +/** + * e_table_memory_store_change_adopt + * @etms: the ETabelMemoryStore. + * @row: the row we're changing. + * @data: the new closure to associate with this row. + * + * a varargs version of e_table_memory_store_change_adopt_array. you + * must pass in etms->col_count args. + */ +void +e_table_memory_store_change_adopt (ETableMemoryStore *etms, + gint row, + gpointer data, + ...) +{ + gpointer *store; + va_list args; + gint i; + + g_return_if_fail (row >= 0 && row < e_table_model_row_count (E_TABLE_MODEL (etms))); + + store = g_new0 (gpointer , etms->priv->col_count + 1); + + va_start (args, data); + for (i = 0; i < etms->priv->col_count; i++) { + store[i] = va_arg (args, gpointer); + } + va_end (args); + + e_table_memory_store_change_adopt_array (etms, row, store, data); + + g_free (store); +} + +void +e_table_memory_store_remove (ETableMemoryStore *etms, + gint row) +{ + ETableModel *model; + gint column_count, row_count; + gint i; + + model = E_TABLE_MODEL (etms); + column_count = e_table_model_column_count (model); + + for (i = 0; i < column_count; i++) + e_table_model_free_value (model, i, e_table_model_value_at (model, i, row)); + + row_count = e_table_model_row_count (E_TABLE_MODEL (etms)) - 1; + memmove ( + etms->priv->store + etms->priv->col_count * row, + etms->priv->store + etms->priv->col_count * (row + 1), + etms->priv->col_count * (row_count - row) * sizeof (gpointer)); + etms->priv->store = g_realloc (etms->priv->store, etms->priv->col_count * row_count * sizeof (gpointer)); + + e_table_memory_remove (E_TABLE_MEMORY (etms), row); +} + +void +e_table_memory_store_clear (ETableMemoryStore *etms) +{ + ETableModel *model; + gint row_count, column_count; + gint i, j; + + model = E_TABLE_MODEL (etms); + row_count = e_table_model_row_count (model); + column_count = e_table_model_column_count (model); + + for (i = 0; i < row_count; i++) { + for (j = 0; j < column_count; j++) { + e_table_model_free_value (model, j, e_table_model_value_at (model, j, i)); + } + } + + e_table_memory_clear (E_TABLE_MEMORY (etms)); + + g_free (etms->priv->store); + etms->priv->store = NULL; +} diff --git a/e-util/e-table-memory-store.h b/e-util/e-table-memory-store.h new file mode 100644 index 0000000000..c8167f8608 --- /dev/null +++ b/e-util/e-table-memory-store.h @@ -0,0 +1,155 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_MEMORY_STORE_H_ +#define _E_TABLE_MEMORY_STORE_H_ + +#include <e-util/e-table-memory.h> +#include <e-util/e-table-memory-callbacks.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_MEMORY_STORE \ + (e_table_memory_store_get_type ()) +#define E_TABLE_MEMORY_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_MEMORY_STORE, ETableMemoryStore)) +#define E_TABLE_MEMORY_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_MEMORY_STORE, ETableMemoryStoreClass)) +#define E_IS_TABLE_MEMORY_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_MEMORY_STORE)) +#define E_IS_TABLE_MEMORY_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_MEMORY_STORE)) +#define E_TABLE_MEMORY_STORE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_MEMORY_STORE, ETableMemoryStoreClass)) + +G_BEGIN_DECLS + +typedef enum { + E_TABLE_MEMORY_STORE_COLUMN_TYPE_TERMINATOR, + E_TABLE_MEMORY_STORE_COLUMN_TYPE_INTEGER, + E_TABLE_MEMORY_STORE_COLUMN_TYPE_STRING, + E_TABLE_MEMORY_STORE_COLUMN_TYPE_PIXBUF, + E_TABLE_MEMORY_STORE_COLUMN_TYPE_OBJECT, + E_TABLE_MEMORY_STORE_COLUMN_TYPE_CUSTOM +} ETableMemoryStoreColumnType; + +typedef struct { + ETableMemoryCallbacksDuplicateValueFn duplicate_value; + ETableMemoryCallbacksFreeValueFn free_value; + ETableMemoryCallbacksInitializeValueFn initialize_value; + ETableMemoryCallbacksValueIsEmptyFn value_is_empty; + ETableMemoryCallbacksValueToStringFn value_to_string; +} ETableMemoryStoreCustomColumn; + +typedef struct { + ETableMemoryStoreColumnType type; + ETableMemoryStoreCustomColumn custom; + guint editable : 1; +} ETableMemoryStoreColumnInfo; + +#define E_TABLE_MEMORY_STORE_TERMINATOR \ + { E_TABLE_MEMORY_STORE_COLUMN_TYPE_TERMINATOR, { NULL }, FALSE } +#define E_TABLE_MEMORY_STORE_INTEGER \ + { E_TABLE_MEMORY_STORE_COLUMN_TYPE_INTEGER, { NULL }, FALSE } +#define E_TABLE_MEMORY_STORE_STRING \ + { E_TABLE_MEMORY_STORE_COLUMN_TYPE_STRING, { NULL }, FALSE } + +typedef struct _ETableMemoryStore ETableMemoryStore; +typedef struct _ETableMemoryStoreClass ETableMemoryStoreClass; +typedef struct _ETableMemoryStorePrivate ETableMemoryStorePrivate; + +struct _ETableMemoryStore { + ETableMemory parent; + ETableMemoryStorePrivate *priv; +}; + +struct _ETableMemoryStoreClass { + ETableMemoryClass parent_class; +}; + +GType e_table_memory_store_get_type (void) G_GNUC_CONST; +ETableModel * e_table_memory_store_new (ETableMemoryStoreColumnInfo *columns); +ETableModel * e_table_memory_store_construct (ETableMemoryStore *store, + ETableMemoryStoreColumnInfo *columns); + +/* Adopt a value instead of copying it. */ +void e_table_memory_store_adopt_value_at + (ETableMemoryStore *etms, + gint col, + gint row, + gpointer value); + +/* The size of these arrays is the number of columns. */ +void e_table_memory_store_insert_array + (ETableMemoryStore *etms, + gint row, + gpointer *store, + gpointer data); +void e_table_memory_store_insert (ETableMemoryStore *etms, + gint row, + gpointer data, + ...); +void e_table_memory_store_insert_adopt + (ETableMemoryStore *etms, + gint row, + gpointer data, + ...); +void e_table_memory_store_insert_adopt_array + (ETableMemoryStore *etms, + gint row, + gpointer *store, + gpointer data); +void e_table_memory_store_change_array + (ETableMemoryStore *etms, + gint row, + gpointer *store, + gpointer data); +void e_table_memory_store_change (ETableMemoryStore *etms, + gint row, + gpointer data, + ...); +void e_table_memory_store_change_adopt + (ETableMemoryStore *etms, + gint row, + gpointer data, + ...); +void e_table_memory_store_change_adopt_array + (ETableMemoryStore *etms, + gint row, + gpointer *store, + gpointer data); +void e_table_memory_store_remove (ETableMemoryStore *etms, + gint row); +void e_table_memory_store_clear (ETableMemoryStore *etms); + +G_END_DECLS + +#endif /* _E_TABLE_MEMORY_STORE_H_ */ diff --git a/e-util/e-table-memory.c b/e-util/e-table-memory.c new file mode 100644 index 0000000000..b9a7eb94e7 --- /dev/null +++ b/e-util/e-table-memory.c @@ -0,0 +1,271 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <errno.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> + +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include "e-table-memory.h" +#include "e-xml-utils.h" + +#define E_TABLE_MEMORY_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TABLE_MEMORY, ETableMemoryPrivate)) + +G_DEFINE_TYPE (ETableMemory, e_table_memory, E_TYPE_TABLE_MODEL) + +struct _ETableMemoryPrivate { + gpointer *data; + gint num_rows; + gint frozen; +}; + +static void +etmm_finalize (GObject *object) +{ + ETableMemoryPrivate *priv; + + priv = E_TABLE_MEMORY_GET_PRIVATE (object); + + g_free (priv->data); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_table_memory_parent_class)->finalize (object); +} + +static gint +etmm_row_count (ETableModel *etm) +{ + ETableMemory *etmm = E_TABLE_MEMORY (etm); + + return etmm->priv->num_rows; +} + +static void +e_table_memory_class_init (ETableMemoryClass *class) +{ + GObjectClass *object_class; + ETableModelClass *table_model_class; + + g_type_class_add_private (class, sizeof (ETableMemoryPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = etmm_finalize; + + table_model_class = E_TABLE_MODEL_CLASS (class); + table_model_class->row_count = etmm_row_count; +} + +static void +e_table_memory_init (ETableMemory *etmm) +{ + etmm->priv = E_TABLE_MEMORY_GET_PRIVATE (etmm); +} + +/** + * e_table_memory_new + * + * XXX docs here. + * + * return values: a newly constructed ETableMemory. + */ +ETableMemory * +e_table_memory_new (void) +{ + return g_object_new (E_TYPE_TABLE_MEMORY, NULL); +} + +/** + * e_table_memory_get_data: + * @etmm: + * @row: + * + * + * + * Return value: + **/ +gpointer +e_table_memory_get_data (ETableMemory *etmm, + gint row) +{ + g_return_val_if_fail (row >= 0, NULL); + g_return_val_if_fail (row < etmm->priv->num_rows, NULL); + + return etmm->priv->data[row]; +} + +/** + * e_table_memory_set_data: + * @etmm: + * @row: + * @data: + * + * + **/ +void +e_table_memory_set_data (ETableMemory *etmm, + gint row, + gpointer data) +{ + g_return_if_fail (row >= 0); + g_return_if_fail (row < etmm->priv->num_rows); + + etmm->priv->data[row] = data; +} + +/** + * e_table_memory_insert: + * @table_model: + * @parent_path: + * @position: + * @data: + * + * + * + * Return value: + **/ +void +e_table_memory_insert (ETableMemory *etmm, + gint row, + gpointer data) +{ + g_return_if_fail (row >= -1); + g_return_if_fail (row <= etmm->priv->num_rows); + + if (!etmm->priv->frozen) + e_table_model_pre_change (E_TABLE_MODEL (etmm)); + + if (row == -1) + row = etmm->priv->num_rows; + etmm->priv->data = g_renew (gpointer, etmm->priv->data, etmm->priv->num_rows + 1); + memmove ( + etmm->priv->data + row + 1, + etmm->priv->data + row, + (etmm->priv->num_rows - row) * sizeof (gpointer)); + etmm->priv->data[row] = data; + etmm->priv->num_rows++; + if (!etmm->priv->frozen) + e_table_model_row_inserted (E_TABLE_MODEL (etmm), row); +} + +/** + * e_table_memory_remove: + * @etable: + * @path: + * + * + * + * Return value: + **/ +gpointer +e_table_memory_remove (ETableMemory *etmm, + gint row) +{ + gpointer ret; + + g_return_val_if_fail (row >= 0, NULL); + g_return_val_if_fail (row < etmm->priv->num_rows, NULL); + + if (!etmm->priv->frozen) + e_table_model_pre_change (E_TABLE_MODEL (etmm)); + ret = etmm->priv->data[row]; + memmove ( + etmm->priv->data + row, + etmm->priv->data + row + 1, + (etmm->priv->num_rows - row - 1) * sizeof (gpointer)); + etmm->priv->num_rows--; + if (!etmm->priv->frozen) + e_table_model_row_deleted (E_TABLE_MODEL (etmm), row); + return ret; +} + +/** + * e_table_memory_clear: + * @etable: + * @path: + * + * + * + * Return value: + **/ +void +e_table_memory_clear (ETableMemory *etmm) +{ + if (!etmm->priv->frozen) + e_table_model_pre_change (E_TABLE_MODEL (etmm)); + g_free (etmm->priv->data); + etmm->priv->data = NULL; + etmm->priv->num_rows = 0; + if (!etmm->priv->frozen) + e_table_model_changed (E_TABLE_MODEL (etmm)); +} + +/** + * e_table_memory_freeze: + * @etmm: the ETableModel to freeze. + * + * This function prepares an ETableModel for a period of much change. + * All signals regarding changes to the table are deferred until we + * thaw the table. + * + **/ +void +e_table_memory_freeze (ETableMemory *etmm) +{ + ETableMemoryPrivate *priv = etmm->priv; + + if (priv->frozen == 0) + e_table_model_pre_change (E_TABLE_MODEL (etmm)); + + priv->frozen++; +} + +/** + * e_table_memory_thaw: + * @etmm: the ETableMemory to thaw. + * + * This function thaws an ETableMemory. All the defered signals can add + * up to a lot, we don't know - so we just emit a model_changed + * signal. + * + **/ +void +e_table_memory_thaw (ETableMemory *etmm) +{ + ETableMemoryPrivate *priv = etmm->priv; + + if (priv->frozen > 0) + priv->frozen--; + if (priv->frozen == 0) { + e_table_model_changed (E_TABLE_MODEL (etmm)); + } +} diff --git a/e-util/e-table-memory.h b/e-util/e-table-memory.h new file mode 100644 index 0000000000..8762027eaf --- /dev/null +++ b/e-util/e-table-memory.h @@ -0,0 +1,91 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_MEMORY_H_ +#define _E_TABLE_MEMORY_H_ + +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include <e-util/e-table-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_MEMORY \ + (e_table_memory_get_type ()) +#define E_TABLE_MEMORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_MEMORY, ETableMemory)) +#define E_TABLE_MEMORY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_MEMORY, ETableMemoryClass)) +#define E_IS_TABLE_MEMORY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_MEMORY)) +#define E_IS_TABLE_MEMORY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_MEMORY)) +#define E_TABLE_MEMORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_MEMORY, ETableMemoryClass)) + +G_BEGIN_DECLS + +typedef struct _ETableMemory ETableMemory; +typedef struct _ETableMemoryClass ETableMemoryClass; +typedef struct _ETableMemoryPrivate ETableMemoryPrivate; + +struct _ETableMemory { + ETableModel parent; + ETableMemoryPrivate *priv; +}; + +struct _ETableMemoryClass { + ETableModelClass parent_class; +}; + +GType e_table_memory_get_type (void) G_GNUC_CONST; +ETableMemory * e_table_memory_new (void); +void e_table_memory_construct (ETableMemory *etable); + +/* row operations */ +void e_table_memory_insert (ETableMemory *etable, + gint row, + gpointer data); +gpointer e_table_memory_remove (ETableMemory *etable, + gint row); +void e_table_memory_clear (ETableMemory *etable); + +/* Freeze and thaw */ +void e_table_memory_freeze (ETableMemory *etable); +void e_table_memory_thaw (ETableMemory *etable); +gpointer e_table_memory_get_data (ETableMemory *etm, + gint row); +void e_table_memory_set_data (ETableMemory *etm, + gint row, + gpointer data); + +G_END_DECLS + +#endif /* _E_TABLE_MEMORY_H */ diff --git a/e-util/e-table-model.c b/e-util/e-table-model.c new file mode 100644 index 0000000000..1ae4d3e81b --- /dev/null +++ b/e-util/e-table-model.c @@ -0,0 +1,682 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-model.h" + +#include "e-marshal.h" + +#define ETM_FROZEN(e) \ + (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (e), "frozen")) != 0) + +#define d(x) + +d (static gint depth = 0;) + +G_DEFINE_TYPE (ETableModel, e_table_model, G_TYPE_OBJECT) + +enum { + MODEL_NO_CHANGE, + MODEL_CHANGED, + MODEL_PRE_CHANGE, + MODEL_ROW_CHANGED, + MODEL_CELL_CHANGED, + MODEL_ROWS_INSERTED, + MODEL_ROWS_DELETED, + ROW_SELECTION, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +/** + * e_table_model_column_count: + * @e_table_model: The e-table-model to operate on + * + * Returns: the number of columns in the table model. + */ +gint +e_table_model_column_count (ETableModel *e_table_model) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), 0); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + g_return_val_if_fail (class->column_count != NULL, 0); + + return class->column_count (e_table_model); +} + +/** + * e_table_model_row_count: + * @e_table_model: the e-table-model to operate on + * + * Returns: the number of rows in the Table model. + */ +gint +e_table_model_row_count (ETableModel *e_table_model) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), 0); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + g_return_val_if_fail (class->row_count != NULL, 0); + + return class->row_count (e_table_model); +} + +/** + * e_table_model_append_row: + * @e_table_model: the table model to append the a row to. + * @source: + * @row: + * + */ +void +e_table_model_append_row (ETableModel *e_table_model, + ETableModel *source, + gint row) +{ + ETableModelClass *class; + + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + + if (class->append_row != NULL) + class->append_row (e_table_model, source, row); +} + +/** + * e_table_value_at: + * @e_table_model: the e-table-model to operate on + * @col: column in the model to pull data from. + * @row: row in the model to pull data from. + * + * Return value: This function returns the value that is stored + * by the @e_table_model in column @col and row @row. The data + * returned can be a pointer or any data value that can be stored + * inside a pointer. + * + * The data returned is typically used by an ECell renderer. + * + * The data returned must be valid until the model sends a signal that + * affect that piece of data. model_changed affects all data. + * row_changed affects the data in that row. cell_changed affects the + * data in that cell. rows_deleted affects all data in those rows. + * rows_inserted and no_change don't affect any data in this way. + **/ +gpointer +e_table_model_value_at (ETableModel *e_table_model, + gint col, + gint row) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), NULL); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + g_return_val_if_fail (class->value_at != NULL, NULL); + + return class->value_at (e_table_model, col, row); +} + +/** + * e_table_model_set_value_at: + * @e_table_model: the table model to operate on. + * @col: the column where the data will be stored in the model. + * @row: the row where the data will be stored in the model. + * @value: the data to be stored. + * + * This function instructs the model to store the value in @data in the + * the @e_table_model at column @col and row @row. The @data typically + * comes from one of the ECell rendering objects. + * + * There should be an agreement between the Table Model and the user + * of this function about the data being stored. Typically it will + * be a pointer to a set of data, or a datum that fits inside a gpointer . + */ +void +e_table_model_set_value_at (ETableModel *e_table_model, + gint col, + gint row, + gconstpointer value) +{ + ETableModelClass *class; + + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + g_return_if_fail (class->set_value_at != NULL); + + class->set_value_at (e_table_model, col, row, value); +} + +/** + * e_table_model_is_cell_editable: + * @e_table_model: the table model to query. + * @col: column to query. + * @row: row to query. + * + * Returns: %TRUE if the cell in @e_table_model at @col,@row can be + * edited, %FALSE otherwise + */ +gboolean +e_table_model_is_cell_editable (ETableModel *e_table_model, + gint col, + gint row) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), FALSE); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + g_return_val_if_fail (class->is_cell_editable != NULL, FALSE); + + return class->is_cell_editable (e_table_model, col, row); +} + +gpointer +e_table_model_duplicate_value (ETableModel *e_table_model, + gint col, + gconstpointer value) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), NULL); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + + if (class->duplicate_value == NULL) + return NULL; + + return class->duplicate_value (e_table_model, col, value); +} + +void +e_table_model_free_value (ETableModel *e_table_model, + gint col, + gpointer value) +{ + ETableModelClass *class; + + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + + if (class->free_value != NULL) + class->free_value (e_table_model, col, value); +} + +gboolean +e_table_model_has_save_id (ETableModel *e_table_model) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), FALSE); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + + if (class->has_save_id == NULL) + return FALSE; + + return class->has_save_id (e_table_model); +} + +gchar * +e_table_model_get_save_id (ETableModel *e_table_model, + gint row) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), NULL); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + + if (class->get_save_id == NULL) + return NULL; + + return class->get_save_id (e_table_model, row); +} + +gboolean +e_table_model_has_change_pending (ETableModel *e_table_model) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), FALSE); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + + if (class->has_change_pending == NULL) + return FALSE; + + return class->has_change_pending (e_table_model); +} + +gpointer +e_table_model_initialize_value (ETableModel *e_table_model, + gint col) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), NULL); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + + if (class->initialize_value == NULL) + return NULL; + + return class->initialize_value (e_table_model, col); +} + +gboolean +e_table_model_value_is_empty (ETableModel *e_table_model, + gint col, + gconstpointer value) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), FALSE); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + + if (class->value_is_empty == NULL) + return FALSE; + + return class->value_is_empty (e_table_model, col, value); +} + +gchar * +e_table_model_value_to_string (ETableModel *e_table_model, + gint col, + gconstpointer value) +{ + ETableModelClass *class; + + g_return_val_if_fail (E_IS_TABLE_MODEL (e_table_model), NULL); + + class = E_TABLE_MODEL_GET_CLASS (e_table_model); + + if (class->value_to_string == NULL) + return g_strdup (""); + + return class->value_to_string (e_table_model, col, value); +} + +static void +e_table_model_class_init (ETableModelClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + signals[MODEL_NO_CHANGE] = g_signal_new ( + "model_no_change", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableModelClass, model_no_change), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[MODEL_CHANGED] = g_signal_new ( + "model_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableModelClass, model_changed), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[MODEL_PRE_CHANGE] = g_signal_new ( + "model_pre_change", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableModelClass, model_pre_change), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[MODEL_ROW_CHANGED] = g_signal_new ( + "model_row_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableModelClass, model_row_changed), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + signals[MODEL_CELL_CHANGED] = g_signal_new ( + "model_cell_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableModelClass, model_cell_changed), + (GSignalAccumulator) NULL, NULL, + e_marshal_VOID__INT_INT, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_INT); + + signals[MODEL_ROWS_INSERTED] = g_signal_new ( + "model_rows_inserted", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableModelClass, model_rows_inserted), + (GSignalAccumulator) NULL, NULL, + e_marshal_VOID__INT_INT, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_INT); + + signals[MODEL_ROWS_DELETED] = g_signal_new ( + "model_rows_deleted", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableModelClass, model_rows_deleted), + (GSignalAccumulator) NULL, NULL, + e_marshal_VOID__INT_INT, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_INT); + + class->column_count = NULL; + class->row_count = NULL; + class->append_row = NULL; + + class->value_at = NULL; + class->set_value_at = NULL; + class->is_cell_editable = NULL; + + class->has_save_id = NULL; + class->get_save_id = NULL; + + class->has_change_pending = NULL; + + class->duplicate_value = NULL; + class->free_value = NULL; + class->initialize_value = NULL; + class->value_is_empty = NULL; + class->value_to_string = NULL; + + class->model_no_change = NULL; + class->model_changed = NULL; + class->model_row_changed = NULL; + class->model_cell_changed = NULL; + class->model_rows_inserted = NULL; + class->model_rows_deleted = NULL; +} + +static void +e_table_model_init (ETableModel *e_table_model) +{ + /* nothing to do */ +} + +#if d(!)0 +static void +print_tabs (void) +{ + gint i; + for (i = 0; i < depth; i++) + g_print ("\t"); +} +#endif + +void +e_table_model_pre_change (ETableModel *e_table_model) +{ + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + if (ETM_FROZEN (e_table_model)) + return; + + d (print_tabs ()); + d (depth++); + g_signal_emit (e_table_model, signals[MODEL_PRE_CHANGE], 0); + d (depth--); +} + +/** + * e_table_model_no_change: + * @e_table_model: the table model to notify of the lack of a change + * + * Use this function to notify any views of this table model that + * the contents of the table model have changed. This will emit + * the signal "model_no_change" on the @e_table_model object. + * + * It is preferable to use the e_table_model_row_changed() and + * the e_table_model_cell_changed() to notify of smaller changes + * than to invalidate the entire model, as the views might have + * ways of caching the information they render from the model. + */ +void +e_table_model_no_change (ETableModel *e_table_model) +{ + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + if (ETM_FROZEN (e_table_model)) + return; + + d (print_tabs ()); + d (depth++); + g_signal_emit (e_table_model, signals[MODEL_NO_CHANGE], 0); + d (depth--); +} + +/** + * e_table_model_changed: + * @e_table_model: the table model to notify of the change + * + * Use this function to notify any views of this table model that + * the contents of the table model have changed. This will emit + * the signal "model_changed" on the @e_table_model object. + * + * It is preferable to use the e_table_model_row_changed() and + * the e_table_model_cell_changed() to notify of smaller changes + * than to invalidate the entire model, as the views might have + * ways of caching the information they render from the model. + */ +void +e_table_model_changed (ETableModel *e_table_model) +{ + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + if (ETM_FROZEN (e_table_model)) + return; + + d (print_tabs ()); + d (depth++); + g_signal_emit (e_table_model, signals[MODEL_CHANGED], 0); + d (depth--); +} + +/** + * e_table_model_row_changed: + * @e_table_model: the table model to notify of the change + * @row: the row that was changed in the model. + * + * Use this function to notify any views of the table model that + * the contents of row @row have changed in model. This function + * will emit the "model_row_changed" signal on the @e_table_model + * object + */ +void +e_table_model_row_changed (ETableModel *e_table_model, + gint row) +{ + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + if (ETM_FROZEN (e_table_model)) + return; + + d (print_tabs ()); + d (depth++); + g_signal_emit (e_table_model, signals[MODEL_ROW_CHANGED], 0, row); + d (depth--); +} + +/** + * e_table_model_cell_changed: + * @e_table_model: the table model to notify of the change + * @col: the column. + * @row: the row + * + * Use this function to notify any views of the table model that + * contents of the cell at @col,@row has changed. This will emit + * the "model_cell_changed" signal on the @e_table_model + * object + */ +void +e_table_model_cell_changed (ETableModel *e_table_model, + gint col, + gint row) +{ + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + if (ETM_FROZEN (e_table_model)) + return; + + d (print_tabs ()); + d (depth++); + g_signal_emit ( + e_table_model, signals[MODEL_CELL_CHANGED], 0, col, row); + d (depth--); +} + +/** + * e_table_model_rows_inserted: + * @e_table_model: the table model to notify of the change + * @row: the row that was inserted into the model. + * @count: The number of rows that were inserted. + * + * Use this function to notify any views of the table model that + * @count rows at row @row have been inserted into the model. This + * function will emit the "model_rows_inserted" signal on the + * @e_table_model object + */ +void +e_table_model_rows_inserted (ETableModel *e_table_model, + gint row, + gint count) +{ + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + if (ETM_FROZEN (e_table_model)) + return; + + d (print_tabs ()); + d (depth++); + g_signal_emit ( + e_table_model, signals[MODEL_ROWS_INSERTED], 0, row, count); + d (depth--); +} + +/** + * e_table_model_row_inserted: + * @e_table_model: the table model to notify of the change + * @row: the row that was inserted into the model. + * + * Use this function to notify any views of the table model that the + * row @row has been inserted into the model. This function will emit + * the "model_rows_inserted" signal on the @e_table_model object + */ +void +e_table_model_row_inserted (ETableModel *e_table_model, + gint row) +{ + e_table_model_rows_inserted (e_table_model, row, 1); +} + +/** + * e_table_model_row_deleted: + * @e_table_model: the table model to notify of the change + * @row: the row that was deleted + * @count: The number of rows deleted + * + * Use this function to notify any views of the table model that + * @count rows at row @row have been deleted from the model. This + * function will emit the "model_rows_deleted" signal on the + * @e_table_model object + */ +void +e_table_model_rows_deleted (ETableModel *e_table_model, + gint row, + gint count) +{ + g_return_if_fail (E_IS_TABLE_MODEL (e_table_model)); + + if (ETM_FROZEN (e_table_model)) + return; + + d (print_tabs ()); + d (depth++); + g_signal_emit ( + e_table_model, signals[MODEL_ROWS_DELETED], 0, row, count); + d (depth--); +} + +/** + * e_table_model_row_deleted: + * @e_table_model: the table model to notify of the change + * @row: the row that was deleted + * + * Use this function to notify any views of the table model that the + * row @row has been deleted from the model. This function will emit + * the "model_rows_deleted" signal on the @e_table_model object + */ +void +e_table_model_row_deleted (ETableModel *e_table_model, + gint row) +{ + e_table_model_rows_deleted (e_table_model, row, 1); +} + +void +e_table_model_freeze (ETableModel *e_table_model) +{ + e_table_model_pre_change (e_table_model); + + /* FIXME This expression is awesome! */ + g_object_set_data ( + G_OBJECT (e_table_model), "frozen", + GINT_TO_POINTER (GPOINTER_TO_INT ( + g_object_get_data (G_OBJECT (e_table_model), "frozen")) + 1)); +} + +void +e_table_model_thaw (ETableModel *e_table_model) +{ + /* FIXME This expression is awesome! */ + g_object_set_data ( + G_OBJECT (e_table_model), "frozen", + GINT_TO_POINTER (GPOINTER_TO_INT ( + g_object_get_data (G_OBJECT (e_table_model), "frozen")) - 1)); + + e_table_model_changed (e_table_model); +} + diff --git a/e-util/e-table-model.h b/e-util/e-table-model.h new file mode 100644 index 0000000000..3ed188e11c --- /dev/null +++ b/e-util/e-table-model.h @@ -0,0 +1,217 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_MODEL_H_ +#define _E_TABLE_MODEL_H_ + +#include <glib-object.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_MODEL \ + (e_table_model_get_type ()) +#define E_TABLE_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_MODEL, ETableModel)) +#define E_TABLE_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_MODEL, ETableModelClass)) +#define E_IS_TABLE_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_MODEL)) +#define E_IS_TABLE_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_MODEL)) +#define E_TABLE_MODEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_MODEL, ETableModelClass)) + +G_BEGIN_DECLS + +typedef struct _ETableModel ETableModel; +typedef struct _ETableModelClass ETableModelClass; + +struct _ETableModel { + GObject parent; +}; + +struct _ETableModelClass { + GObjectClass parent_class; + + /* + * Virtual methods + */ + gint (*column_count) (ETableModel *etm); + gint (*row_count) (ETableModel *etm); + void (*append_row) (ETableModel *etm, + ETableModel *source, + gint row); + + gpointer (*value_at) (ETableModel *etm, + gint col, + gint row); + void (*set_value_at) (ETableModel *etm, + gint col, + gint row, + gconstpointer value); + gboolean (*is_cell_editable) (ETableModel *etm, + gint col, + gint row); + + gboolean (*has_save_id) (ETableModel *etm); + gchar * (*get_save_id) (ETableModel *etm, + gint row); + + gboolean (*has_change_pending) (ETableModel *etm); + + /* Allocate a copy of the given value. */ + gpointer (*duplicate_value) (ETableModel *etm, + gint col, + gconstpointer value); + /* Free an allocated value. */ + void (*free_value) (ETableModel *etm, + gint col, + gpointer value); + /* Return an allocated empty value. */ + gpointer (*initialize_value) (ETableModel *etm, + gint col); + /* Return TRUE if value is equivalent to an empty cell. */ + gboolean (*value_is_empty) (ETableModel *etm, + gint col, + gconstpointer value); + /* Return an allocated string. */ + gchar * (*value_to_string) (ETableModel *etm, + gint col, + gconstpointer value); + + /* + * Signals + */ + + /* + * These all come after the change has been made. + * No changes, cancel pre_change: no_change + * Major structural changes: model_changed + * Changes only in a row: row_changed + * Only changes in a cell: cell_changed + * A row inserted: row_inserted + * A row deleted: row_deleted + */ + void (*model_pre_change) (ETableModel *etm); + + void (*model_no_change) (ETableModel *etm); + void (*model_changed) (ETableModel *etm); + void (*model_row_changed) (ETableModel *etm, + gint row); + void (*model_cell_changed) (ETableModel *etm, + gint col, + gint row); + void (*model_rows_inserted) (ETableModel *etm, + gint row, + gint count); + void (*model_rows_deleted) (ETableModel *etm, + gint row, + gint count); +}; + +GType e_table_model_get_type (void) G_GNUC_CONST; + +/**/ +gint e_table_model_column_count (ETableModel *e_table_model); +const gchar * e_table_model_column_name (ETableModel *e_table_model, + gint col); +gint e_table_model_row_count (ETableModel *e_table_model); +void e_table_model_append_row (ETableModel *e_table_model, + ETableModel *source, + gint row); + +/**/ +gpointer e_table_model_value_at (ETableModel *e_table_model, + gint col, + gint row); +void e_table_model_set_value_at (ETableModel *e_table_model, + gint col, + gint row, + gconstpointer value); +gboolean e_table_model_is_cell_editable (ETableModel *e_table_model, + gint col, + gint row); + +/**/ +gboolean e_table_model_has_save_id (ETableModel *etm); +gchar * e_table_model_get_save_id (ETableModel *etm, + gint row); + +/**/ +gboolean e_table_model_has_change_pending + (ETableModel *etm); + +/**/ +gpointer e_table_model_duplicate_value (ETableModel *e_table_model, + gint col, + gconstpointer value); +void e_table_model_free_value (ETableModel *e_table_model, + gint col, + gpointer value); +gpointer e_table_model_initialize_value (ETableModel *e_table_model, + gint col); +gboolean e_table_model_value_is_empty (ETableModel *e_table_model, + gint col, + gconstpointer value); +gchar * e_table_model_value_to_string (ETableModel *e_table_model, + gint col, + gconstpointer value); + +/* + * Routines for emitting signals on the e_table + */ +void e_table_model_pre_change (ETableModel *e_table_model); +void e_table_model_no_change (ETableModel *e_table_model); +void e_table_model_changed (ETableModel *e_table_model); +void e_table_model_row_changed (ETableModel *e_table_model, + gint row); +void e_table_model_cell_changed (ETableModel *e_table_model, + gint col, + gint row); +void e_table_model_rows_inserted (ETableModel *e_table_model, + gint row, + gint count); +void e_table_model_rows_deleted (ETableModel *e_table_model, + gint row, + gint count); + +/**/ +void e_table_model_row_inserted (ETableModel *e_table_model, + gint row); +void e_table_model_row_deleted (ETableModel *e_table_model, + gint row); + +void e_table_model_freeze (ETableModel *e_table_model); +void e_table_model_thaw (ETableModel *e_table_model); + +G_END_DECLS + +#endif /* _E_TABLE_MODEL_H_ */ diff --git a/e-util/e-table-one.c b/e-util/e-table-one.c new file mode 100644 index 0000000000..db9c27e4d1 --- /dev/null +++ b/e-util/e-table-one.c @@ -0,0 +1,252 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-one.h" + +G_DEFINE_TYPE (ETableOne, e_table_one, E_TYPE_TABLE_MODEL) + +static gint +one_column_count (ETableModel *etm) +{ + ETableOne *one = E_TABLE_ONE (etm); + + if (one->source) + return e_table_model_column_count (one->source); + else + return 0; +} + +static gint +one_row_count (ETableModel *etm) +{ + return 1; +} + +static gpointer +one_value_at (ETableModel *etm, + gint col, + gint row) +{ + ETableOne *one = E_TABLE_ONE (etm); + + if (one->data) + return one->data[col]; + else + return NULL; +} + +static void +one_set_value_at (ETableModel *etm, + gint col, + gint row, + gconstpointer val) +{ + ETableOne *one = E_TABLE_ONE (etm); + + if (one->data && one->source) { + e_table_model_free_value (one->source, col, one->data[col]); + one->data[col] = e_table_model_duplicate_value (one->source, col, val); + } +} + +static gboolean +one_is_cell_editable (ETableModel *etm, + gint col, + gint row) +{ + ETableOne *one = E_TABLE_ONE (etm); + + if (one->source) + return e_table_model_is_cell_editable (one->source, col, -1); + else + return FALSE; +} + +/* The default for one_duplicate_value is to return the raw value. */ +static gpointer +one_duplicate_value (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableOne *one = E_TABLE_ONE (etm); + + if (one->source) + return e_table_model_duplicate_value (one->source, col, value); + else + return (gpointer) value; +} + +static void +one_free_value (ETableModel *etm, + gint col, + gpointer value) +{ + ETableOne *one = E_TABLE_ONE (etm); + + if (one->source) + e_table_model_free_value (one->source, col, value); +} + +static gpointer +one_initialize_value (ETableModel *etm, + gint col) +{ + ETableOne *one = E_TABLE_ONE (etm); + + if (one->source) + return e_table_model_initialize_value (one->source, col); + else + return NULL; +} + +static gboolean +one_value_is_empty (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableOne *one = E_TABLE_ONE (etm); + + if (one->source) + return e_table_model_value_is_empty (one->source, col, value); + else + return FALSE; +} + +static gchar * +one_value_to_string (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableOne *one = E_TABLE_ONE (etm); + + if (one->source) + return e_table_model_value_to_string (one->source, col, value); + else + return g_strdup (""); +} + +static void +one_finalize (GObject *object) +{ + G_OBJECT_CLASS (e_table_one_parent_class)->finalize (object); +} + +static void +one_dispose (GObject *object) +{ + ETableOne *one = E_TABLE_ONE (object); + + if (one->data) { + gint i; + gint col_count; + + if (one->source) { + col_count = e_table_model_column_count (one->source); + + for (i = 0; i < col_count; i++) + e_table_model_free_value (one->source, i, one->data[i]); + } + + g_free (one->data); + } + one->data = NULL; + + if (one->source) + g_object_unref (one->source); + one->source = NULL; + + G_OBJECT_CLASS (e_table_one_parent_class)->dispose (object); +} + +static void +e_table_one_class_init (ETableOneClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + ETableModelClass *model_class = E_TABLE_MODEL_CLASS (class); + + model_class->column_count = one_column_count; + model_class->row_count = one_row_count; + model_class->value_at = one_value_at; + model_class->set_value_at = one_set_value_at; + model_class->is_cell_editable = one_is_cell_editable; + model_class->duplicate_value = one_duplicate_value; + model_class->free_value = one_free_value; + model_class->initialize_value = one_initialize_value; + model_class->value_is_empty = one_value_is_empty; + model_class->value_to_string = one_value_to_string; + + object_class->dispose = one_dispose; + object_class->finalize = one_finalize; +} + +static void +e_table_one_init (ETableOne *one) +{ + one->source = NULL; + one->data = NULL; +} + +ETableModel * +e_table_one_new (ETableModel *source) +{ + ETableOne *eto; + gint col_count; + gint i; + + eto = g_object_new (E_TYPE_TABLE_ONE, NULL); + eto->source = source; + + col_count = e_table_model_column_count (source); + eto->data = g_new (gpointer , col_count); + for (i = 0; i < col_count; i++) { + eto->data[i] = e_table_model_initialize_value (source, i); + } + + if (source) + g_object_ref (source); + + return (ETableModel *) eto; +} + +void +e_table_one_commit (ETableOne *one) +{ + if (one->source) { + gint empty = TRUE; + gint col; + gint cols = e_table_model_column_count (one->source); + for (col = 0; col < cols; col++) { + if (!e_table_model_value_is_empty (one->source, col, one->data[col])) { + empty = FALSE; + break; + } + } + if (!empty) { + e_table_model_append_row (one->source, E_TABLE_MODEL (one), 0); + } + } +} diff --git a/e-util/e-table-one.h b/e-util/e-table-one.h new file mode 100644 index 0000000000..86f5538ffb --- /dev/null +++ b/e-util/e-table-one.h @@ -0,0 +1,75 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_ONE_H_ +#define _E_TABLE_ONE_H_ + +#include <e-util/e-table-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_ONE \ + (e_table_one_get_type ()) +#define E_TABLE_ONE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_ONE, ETableOne)) +#define E_TABLE_ONE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_ONE, ETableOneClass)) +#define E_IS_TABLE_ONE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_ONE)) +#define E_IS_TABLE_ONE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_ONE)) +#define E_TABLE_ONE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_ONE, ETableOneClass)) + +G_BEGIN_DECLS + +typedef struct _ETableOne ETableOne; +typedef struct _ETableOneClass ETableOneClass; + +struct _ETableOne { + ETableModel parent; + + ETableModel *source; + gpointer *data; +}; + +struct _ETableOneClass { + ETableModelClass parent_class; +}; + +GType e_table_one_get_type (void) G_GNUC_CONST; + +ETableModel * e_table_one_new (ETableModel *source); +void e_table_one_commit (ETableOne *one); + +G_END_DECLS + +#endif /* _E_TABLE_ONE_H_ */ + diff --git a/e-util/e-table-search.c b/e-util/e-table-search.c new file mode 100644 index 0000000000..5b6a7bd8d6 --- /dev/null +++ b/e-util/e-table-search.c @@ -0,0 +1,235 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-search.h" + +#include <string.h> + +#include "e-marshal.h" + +#define d(x) + +struct _ETableSearchPrivate { + guint timeout_id; + + gchar *search_string; + gunichar last_character; +}; + +G_DEFINE_TYPE (ETableSearch, e_table_search, G_TYPE_OBJECT) + +enum { + SEARCH_SEARCH, + SEARCH_ACCEPT, + LAST_SIGNAL +}; + +#define E_TABLE_SEARCH_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TABLE_SEARCH, ETableSearchPrivate)) + +d (static gint depth = 0) + +static guint e_table_search_signals[LAST_SIGNAL] = { 0, }; + +static gboolean +e_table_search_search (ETableSearch *e_table_search, + gchar *string, + ETableSearchFlags flags) +{ + gboolean ret_val; + g_return_val_if_fail (E_IS_TABLE_SEARCH (e_table_search), FALSE); + + g_signal_emit ( + e_table_search, + e_table_search_signals[SEARCH_SEARCH], 0, + string, flags, &ret_val); + + return ret_val; +} + +static void +e_table_search_accept (ETableSearch *e_table_search) +{ + g_return_if_fail (E_IS_TABLE_SEARCH (e_table_search)); + + g_signal_emit ( + e_table_search, + e_table_search_signals[SEARCH_ACCEPT], 0); +} + +static gboolean +ets_accept (gpointer data) +{ + ETableSearch *ets = data; + e_table_search_accept (ets); + g_free (ets->priv->search_string); + + ets->priv->timeout_id = 0; + ets->priv->search_string = g_strdup (""); + ets->priv->last_character = 0; + + return FALSE; +} + +static void +drop_timeout (ETableSearch *ets) +{ + if (ets->priv->timeout_id) { + g_source_remove (ets->priv->timeout_id); + } + ets->priv->timeout_id = 0; +} + +static void +add_timeout (ETableSearch *ets) +{ + drop_timeout (ets); + ets->priv->timeout_id = g_timeout_add_seconds (1, ets_accept, ets); +} + +static void +e_table_search_finalize (GObject *object) +{ + ETableSearchPrivate *priv; + + priv = E_TABLE_SEARCH_GET_PRIVATE (object); + + drop_timeout (E_TABLE_SEARCH (object)); + + g_free (priv->search_string); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_table_search_parent_class)->finalize (object); +} + +static void +e_table_search_class_init (ETableSearchClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ETableSearchPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = e_table_search_finalize; + + e_table_search_signals[SEARCH_SEARCH] = g_signal_new ( + "search", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableSearchClass, search), + (GSignalAccumulator) NULL, NULL, + e_marshal_BOOLEAN__STRING_INT, + G_TYPE_BOOLEAN, 2, + G_TYPE_STRING, + G_TYPE_INT); + + e_table_search_signals[SEARCH_ACCEPT] = g_signal_new ( + "accept", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableSearchClass, accept), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + class->search = NULL; + class->accept = NULL; +} + +static void +e_table_search_init (ETableSearch *ets) +{ + ets->priv = E_TABLE_SEARCH_GET_PRIVATE (ets); + + ets->priv->search_string = g_strdup (""); +} + +ETableSearch * +e_table_search_new (void) +{ + return g_object_new (E_TYPE_TABLE_SEARCH, NULL); +} + +/** + * e_table_search_column_count: + * @e_table_search: The e-table-search to operate on + * + * Returns: the number of columns in the table search. + */ +void +e_table_search_input_character (ETableSearch *ets, + gunichar character) +{ + gchar character_utf8[7]; + gchar *temp_string; + + g_return_if_fail (ets != NULL); + g_return_if_fail (E_IS_TABLE_SEARCH (ets)); + + character_utf8[g_unichar_to_utf8 (character, character_utf8)] = 0; + + temp_string = g_strdup_printf ("%s%s", ets->priv->search_string, character_utf8); + if (e_table_search_search ( + ets, temp_string, + ets->priv->last_character != 0 ? + E_TABLE_SEARCH_FLAGS_CHECK_CURSOR_FIRST : 0)) { + g_free (ets->priv->search_string); + ets->priv->search_string = temp_string; + add_timeout (ets); + ets->priv->last_character = character; + return; + } else { + g_free (temp_string); + } + + if (character == ets->priv->last_character) { + if (ets->priv->search_string && + e_table_search_search (ets, ets->priv->search_string, 0)) { + add_timeout (ets); + } + } +} + +gboolean +e_table_search_backspace (ETableSearch *ets) +{ + gchar *end; + + g_return_val_if_fail (ets != NULL, FALSE); + g_return_val_if_fail (E_IS_TABLE_SEARCH (ets), FALSE); + + if (!ets->priv->search_string || + !*ets->priv->search_string) + return FALSE; + + end = ets->priv->search_string + strlen (ets->priv->search_string); + end = g_utf8_prev_char (end); + *end = 0; + ets->priv->last_character = 0; + add_timeout (ets); + return TRUE; +} diff --git a/e-util/e-table-search.h b/e-util/e-table-search.h new file mode 100644 index 0000000000..1348e6487f --- /dev/null +++ b/e-util/e-table-search.h @@ -0,0 +1,86 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SEARCH_H_ +#define _E_TABLE_SEARCH_H_ + +#include <glib-object.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_SEARCH \ + (e_table_search_get_type ()) +#define E_TABLE_SEARCH(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_SEARCH, ETableSearch)) +#define E_TABLE_SEARCH_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_SEARCH, ETableSearchClass)) +#define E_IS_TABLE_SEARCH(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_SEARCH)) +#define E_IS_TABLE_SEARCH_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_SEARCH)) +#define E_TABLE_SEARCH_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_SEARCH, ETableSearchClass)) + +G_BEGIN_DECLS + +typedef struct _ETableSearch ETableSearch; +typedef struct _ETableSearchClass ETableSearchClass; +typedef struct _ETableSearchPrivate ETableSearchPrivate; + +typedef enum { + E_TABLE_SEARCH_FLAGS_CHECK_CURSOR_FIRST = 1 << 0 +} ETableSearchFlags; + +struct _ETableSearch { + GObject parent; + ETableSearchPrivate *priv; +}; + +struct _ETableSearchClass { + GObjectClass parent_class; + + /* Signals */ + gboolean (*search) (ETableSearch *ets, + gchar *string /* utf8 */, + ETableSearchFlags flags); + void (*accept) (ETableSearch *ets); +}; + +GType e_table_search_get_type (void) G_GNUC_CONST; +ETableSearch * e_table_search_new (void); +void e_table_search_input_character (ETableSearch *e_table_search, + gunichar character); +gboolean e_table_search_backspace (ETableSearch *e_table_search); +void e_table_search_cancel (ETableSearch *e_table_search); + +G_END_DECLS + +#endif /* _E_TABLE_SEARCH_H_ */ diff --git a/e-util/e-table-selection-model.c b/e-util/e-table-selection-model.c new file mode 100644 index 0000000000..abe4b0c3ff --- /dev/null +++ b/e-util/e-table-selection-model.c @@ -0,0 +1,384 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include <gdk/gdkkeysyms.h> + +#include <glib/gi18n.h> + +#include "e-table-selection-model.h" + +G_DEFINE_TYPE (ETableSelectionModel, e_table_selection_model, E_SELECTION_MODEL_ARRAY_TYPE) + +static gint etsm_get_row_count (ESelectionModelArray *esm); + +enum { + PROP_0, + PROP_MODEL, + PROP_HEADER +}; + +static void +save_to_hash (gint model_row, + gpointer closure) +{ + ETableSelectionModel *etsm = closure; + const gchar *key = e_table_model_get_save_id (etsm->model, model_row); + + g_hash_table_insert (etsm->hash, (gpointer) key, (gpointer) key); +} + +static void +free_hash (ETableSelectionModel *etsm) +{ + if (etsm->hash) { + g_hash_table_destroy (etsm->hash); + etsm->hash = NULL; + } + if (etsm->cursor_id) + g_free (etsm->cursor_id); + etsm->cursor_id = NULL; +} + +static void +model_pre_change (ETableModel *etm, + ETableSelectionModel *etsm) +{ + free_hash (etsm); + + if (etsm->model && e_table_model_has_save_id (etsm->model)) { + gint cursor_row; + + etsm->hash = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) NULL); + e_selection_model_foreach (E_SELECTION_MODEL (etsm), save_to_hash, etsm); + + g_object_get ( + etsm, + "cursor_row", &cursor_row, + NULL); + g_free (etsm->cursor_id); + if (cursor_row != -1) + etsm->cursor_id = e_table_model_get_save_id (etm, cursor_row); + else + etsm->cursor_id = NULL; + } +} + +static gint +model_changed_idle (ETableSelectionModel *etsm) +{ + ETableModel *etm = etsm->model; + + e_selection_model_clear (E_SELECTION_MODEL (etsm)); + + if (etsm->cursor_id && etm && e_table_model_has_save_id (etm)) { + gint row_count = e_table_model_row_count (etm); + gint cursor_row = -1; + gint cursor_col = -1; + gint i; + e_selection_model_array_confirm_row_count (E_SELECTION_MODEL_ARRAY (etsm)); + for (i = 0; i < row_count; i++) { + gchar *save_id = e_table_model_get_save_id (etm, i); + if (g_hash_table_lookup (etsm->hash, save_id)) + e_selection_model_change_one_row (E_SELECTION_MODEL (etsm), i, TRUE); + + if (etsm->cursor_id && !strcmp (etsm->cursor_id, save_id)) { + cursor_row = i; + cursor_col = e_selection_model_cursor_col (E_SELECTION_MODEL (etsm)); + if (cursor_col == -1) { + if (etsm->eth) { + cursor_col = e_table_header_prioritized_column (etsm->eth); + } else + cursor_col = 0; + } + e_selection_model_change_cursor (E_SELECTION_MODEL (etsm), cursor_row, cursor_col); + g_free (etsm->cursor_id); + etsm->cursor_id = NULL; + } + g_free (save_id); + } + free_hash (etsm); + e_selection_model_cursor_changed (E_SELECTION_MODEL (etsm), cursor_row, cursor_col); + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); + } + etsm->model_changed_idle_id = 0; + return FALSE; +} + +static void +model_changed (ETableModel *etm, + ETableSelectionModel *etsm) +{ + e_selection_model_clear (E_SELECTION_MODEL (etsm)); + if (!etsm->model_changed_idle_id && etm && e_table_model_has_save_id (etm)) { + etsm->model_changed_idle_id = g_idle_add_full (G_PRIORITY_HIGH, (GSourceFunc) model_changed_idle, etsm, NULL); + } +} + +static void +model_row_changed (ETableModel *etm, + gint row, + ETableSelectionModel *etsm) +{ + free_hash (etsm); +} + +static void +model_cell_changed (ETableModel *etm, + gint col, + gint row, + ETableSelectionModel *etsm) +{ + free_hash (etsm); +} + +#if 1 +static void +model_rows_inserted (ETableModel *etm, + gint row, + gint count, + ETableSelectionModel *etsm) +{ + e_selection_model_array_insert_rows (E_SELECTION_MODEL_ARRAY (etsm), row, count); + free_hash (etsm); +} + +static void +model_rows_deleted (ETableModel *etm, + gint row, + gint count, + ETableSelectionModel *etsm) +{ + e_selection_model_array_delete_rows (E_SELECTION_MODEL_ARRAY (etsm), row, count); + free_hash (etsm); +} + +#else + +static void +model_rows_inserted (ETableModel *etm, + gint row, + gint count, + ETableSelectionModel *etsm) +{ + model_changed (etm, etsm); +} + +static void +model_rows_deleted (ETableModel *etm, + gint row, + gint count, + ETableSelectionModel *etsm) +{ + model_changed (etm, etsm); +} +#endif + +inline static void +add_model (ETableSelectionModel *etsm, + ETableModel *model) +{ + etsm->model = model; + if (model) { + g_object_ref (model); + etsm->model_pre_change_id = g_signal_connect ( + model, "model_pre_change", + G_CALLBACK (model_pre_change), etsm); + etsm->model_changed_id = g_signal_connect ( + model, "model_changed", + G_CALLBACK (model_changed), etsm); + etsm->model_row_changed_id = g_signal_connect ( + model, "model_row_changed", + G_CALLBACK (model_row_changed), etsm); + etsm->model_cell_changed_id = g_signal_connect ( + model, "model_cell_changed", + G_CALLBACK (model_cell_changed), etsm); + etsm->model_rows_inserted_id = g_signal_connect ( + model, "model_rows_inserted", + G_CALLBACK (model_rows_inserted), etsm); + etsm->model_rows_deleted_id = g_signal_connect ( + model, "model_rows_deleted", + G_CALLBACK (model_rows_deleted), etsm); + } + e_selection_model_array_confirm_row_count (E_SELECTION_MODEL_ARRAY (etsm)); +} + +inline static void +drop_model (ETableSelectionModel *etsm) +{ + if (etsm->model) { + g_signal_handler_disconnect ( + etsm->model, + etsm->model_pre_change_id); + g_signal_handler_disconnect ( + etsm->model, + etsm->model_changed_id); + g_signal_handler_disconnect ( + etsm->model, + etsm->model_row_changed_id); + g_signal_handler_disconnect ( + etsm->model, + etsm->model_cell_changed_id); + g_signal_handler_disconnect ( + etsm->model, + etsm->model_rows_inserted_id); + g_signal_handler_disconnect ( + etsm->model, + etsm->model_rows_deleted_id); + + g_object_unref (etsm->model); + } + etsm->model = NULL; +} + +static void +etsm_dispose (GObject *object) +{ + ETableSelectionModel *etsm; + + etsm = E_TABLE_SELECTION_MODEL (object); + + if (etsm->model_changed_idle_id) + g_source_remove (etsm->model_changed_idle_id); + etsm->model_changed_idle_id = 0; + + drop_model (etsm); + free_hash (etsm); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_table_selection_model_parent_class)->dispose (object); +} + +static void +etsm_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableSelectionModel *etsm = E_TABLE_SELECTION_MODEL (object); + + switch (property_id) { + case PROP_MODEL: + g_value_set_object (value, etsm->model); + break; + case PROP_HEADER: + g_value_set_object (value, etsm->eth); + break; + } +} + +static void +etsm_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ETableSelectionModel *etsm = E_TABLE_SELECTION_MODEL (object); + + switch (property_id) { + case PROP_MODEL: + drop_model (etsm); + add_model (etsm, g_value_get_object (value) ? E_TABLE_MODEL (g_value_get_object (value)) : NULL); + break; + case PROP_HEADER: + etsm->eth = E_TABLE_HEADER (g_value_get_object (value)); + break; + } +} + +static void +e_table_selection_model_init (ETableSelectionModel *selection) +{ + selection->model = NULL; + selection->hash = NULL; + selection->cursor_id = NULL; + + selection->model_changed_idle_id = 0; +} + +static void +e_table_selection_model_class_init (ETableSelectionModelClass *class) +{ + GObjectClass *object_class; + ESelectionModelArrayClass *esma_class; + + object_class = G_OBJECT_CLASS (class); + esma_class = E_SELECTION_MODEL_ARRAY_CLASS (class); + + object_class->dispose = etsm_dispose; + object_class->get_property = etsm_get_property; + object_class->set_property = etsm_set_property; + + esma_class->get_row_count = etsm_get_row_count; + + g_object_class_install_property ( + object_class, + PROP_MODEL, + g_param_spec_object ( + "model", + "Model", + NULL, + E_TYPE_TABLE_MODEL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_HEADER, + g_param_spec_object ( + "header", + "Header", + NULL, + E_TYPE_TABLE_HEADER, + G_PARAM_READWRITE)); +} + +/** + * e_table_selection_model_new + * + * This routine creates a new #ETableSelectionModel. + * + * Returns: The new #ETableSelectionModel. + */ +ETableSelectionModel * +e_table_selection_model_new (void) +{ + return g_object_new (E_TYPE_TABLE_SELECTION_MODEL, NULL); +} + +static gint +etsm_get_row_count (ESelectionModelArray *esma) +{ + ETableSelectionModel *etsm = E_TABLE_SELECTION_MODEL (esma); + + if (etsm->model) + return e_table_model_row_count (etsm->model); + else + return 0; +} diff --git a/e-util/e-table-selection-model.h b/e-util/e-table-selection-model.h new file mode 100644 index 0000000000..0f955ad4bb --- /dev/null +++ b/e-util/e-table-selection-model.h @@ -0,0 +1,91 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SELECTION_MODEL_H_ +#define _E_TABLE_SELECTION_MODEL_H_ + +#include <e-util/e-selection-model-array.h> +#include <e-util/e-table-header.h> +#include <e-util/e-table-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_SELECTION_MODEL \ + (e_table_selection_model_get_type ()) +#define E_TABLE_SELECTION_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_SELECTION_MODEL, ETableSelectionModel)) +#define E_TABLE_SELECTION_MODEL_CLASS(cls) \ + (G_TYPE - CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_SELECTION_MODEL, ETableSelectionModelClass)) +#define E_IS_TABLE_SELECTION_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_SELECTION_MODEL)) +#define E_IS_TABLE_SELECTION_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_SELECTION_MODEL)) +#define E_TABLE_SELECTION_MODEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_SELECTION_MODEL, ETableSelectionModelClass)) + +G_BEGIN_DECLS + +typedef struct _ETableSelectionModel ETableSelectionModel; +typedef struct _ETableSelectionModelClass ETableSelectionModelClass; + +struct _ETableSelectionModel { + ESelectionModelArray parent; + + ETableModel *model; + ETableHeader *eth; + + guint model_pre_change_id; + guint model_changed_id; + guint model_row_changed_id; + guint model_cell_changed_id; + guint model_rows_inserted_id; + guint model_rows_deleted_id; + + guint model_changed_idle_id; + + guint selection_model_changed : 1; + guint group_info_changed : 1; + + GHashTable *hash; + gchar *cursor_id; +}; + +struct _ETableSelectionModelClass { + ESelectionModelArrayClass parent_class; +}; + +GType e_table_selection_model_get_type (void) G_GNUC_CONST; +ETableSelectionModel * + e_table_selection_model_new (void); + +G_END_DECLS + +#endif /* _E_TABLE_SELECTION_MODEL_H_ */ diff --git a/e-util/e-table-sort-info.c b/e-util/e-table-sort-info.c new file mode 100644 index 0000000000..d2654c55b4 --- /dev/null +++ b/e-util/e-table-sort-info.c @@ -0,0 +1,482 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-sort-info.h" + +#include <string.h> + +#include "e-xml-utils.h" + +#define ETM_CLASS(e) (E_TABLE_SORT_INFO_GET_CLASS (e)) + +G_DEFINE_TYPE (ETableSortInfo , e_table_sort_info, G_TYPE_OBJECT) + +enum { + SORT_INFO_CHANGED, + GROUP_INFO_CHANGED, + LAST_SIGNAL +}; + +static guint e_table_sort_info_signals[LAST_SIGNAL] = { 0, }; + +static void +etsi_finalize (GObject *object) +{ + ETableSortInfo *etsi = E_TABLE_SORT_INFO (object); + + if (etsi->groupings) + g_free (etsi->groupings); + etsi->groupings = NULL; + + if (etsi->sortings) + g_free (etsi->sortings); + etsi->sortings = NULL; + + G_OBJECT_CLASS (e_table_sort_info_parent_class)->finalize (object); +} + +static void +e_table_sort_info_init (ETableSortInfo *info) +{ + info->group_count = 0; + info->groupings = NULL; + info->sort_count = 0; + info->sortings = NULL; + info->frozen = 0; + info->sort_info_changed = 0; + info->group_info_changed = 0; + info->can_group = 1; +} + +static void +e_table_sort_info_class_init (ETableSortInfoClass *class) +{ + GObjectClass * object_class = G_OBJECT_CLASS (class); + + object_class->finalize = etsi_finalize; + + e_table_sort_info_signals[SORT_INFO_CHANGED] = g_signal_new ( + "sort_info_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableSortInfoClass, sort_info_changed), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + e_table_sort_info_signals[GROUP_INFO_CHANGED] = g_signal_new ( + "group_info_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableSortInfoClass, group_info_changed), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + class->sort_info_changed = NULL; + class->group_info_changed = NULL; +} + +static void +e_table_sort_info_sort_info_changed (ETableSortInfo *info) +{ + g_return_if_fail (info != NULL); + g_return_if_fail (E_IS_TABLE_SORT_INFO (info)); + + if (info->frozen) { + info->sort_info_changed = 1; + } else { + g_signal_emit (info, e_table_sort_info_signals[SORT_INFO_CHANGED], 0); + } +} + +static void +e_table_sort_info_group_info_changed (ETableSortInfo *info) +{ + g_return_if_fail (info != NULL); + g_return_if_fail (E_IS_TABLE_SORT_INFO (info)); + + if (info->frozen) { + info->group_info_changed = 1; + } else { + g_signal_emit (info, e_table_sort_info_signals[GROUP_INFO_CHANGED], 0); + } +} + +/** + * e_table_sort_info_freeze: + * @info: The ETableSortInfo object + * + * This functions allows the programmer to cluster various changes to the + * ETableSortInfo (grouping and sorting) without having the object emit + * "group_info_changed" or "sort_info_changed" signals on each change. + * + * To thaw, invoke the e_table_sort_info_thaw() function, which will + * trigger any signals that might have been queued. + */ +void +e_table_sort_info_freeze (ETableSortInfo *info) +{ + info->frozen++; +} + +/** + * e_table_sort_info_thaw: + * @info: The ETableSortInfo object + * + * This functions allows the programmer to cluster various changes to the + * ETableSortInfo (grouping and sorting) without having the object emit + * "group_info_changed" or "sort_info_changed" signals on each change. + * + * This function will flush any pending signals that might be emited by + * this object. + */ +void +e_table_sort_info_thaw (ETableSortInfo *info) +{ + info->frozen--; + if (info->frozen != 0) + return; + + if (info->sort_info_changed) { + info->sort_info_changed = 0; + e_table_sort_info_sort_info_changed (info); + } + if (info->group_info_changed) { + info->group_info_changed = 0; + e_table_sort_info_group_info_changed (info); + } +} + +/** + * e_table_sort_info_grouping_get_count: + * @info: The ETableSortInfo object + * + * Returns: the number of grouping criteria in the object. + */ +guint +e_table_sort_info_grouping_get_count (ETableSortInfo *info) +{ + if (info->can_group) + return info->group_count; + else + return 0; +} + +static void +e_table_sort_info_grouping_real_truncate (ETableSortInfo *info, + gint length) +{ + if (length < info->group_count) { + info->group_count = length; + } + if (length > info->group_count) { + info->groupings = g_realloc (info->groupings, length * sizeof (ETableSortColumn)); + info->group_count = length; + } +} + +/** + * e_table_sort_info_grouping_truncate: + * @info: The ETableSortInfo object + * @lenght: position where the truncation happens. + * + * This routine can be used to reduce or grow the number of grouping + * criteria in the object. + */ +void +e_table_sort_info_grouping_truncate (ETableSortInfo *info, + gint length) +{ + e_table_sort_info_grouping_real_truncate (info, length); + e_table_sort_info_group_info_changed (info); +} + +/** + * e_table_sort_info_grouping_get_nth: + * @info: The ETableSortInfo object + * @n: Item information to fetch. + * + * Returns: the description of the @n-th grouping criteria in the @info object. + */ +ETableSortColumn +e_table_sort_info_grouping_get_nth (ETableSortInfo *info, + gint n) +{ + if (info->can_group && n < info->group_count) { + return info->groupings[n]; + } else { + ETableSortColumn fake = {0, 0}; + return fake; + } +} + +/** + * e_table_sort_info_grouping_set_nth: + * @info: The ETableSortInfo object + * @n: Item information to fetch. + * @column: new values for the grouping + * + * Sets the grouping criteria for index @n to be given by @column (a column number and + * whether it is ascending or descending). + */ +void +e_table_sort_info_grouping_set_nth (ETableSortInfo *info, + gint n, + ETableSortColumn column) +{ + if (n >= info->group_count) { + e_table_sort_info_grouping_real_truncate (info, n + 1); + } + info->groupings[n] = column; + e_table_sort_info_group_info_changed (info); +} + +/** + * e_table_sort_info_get_count: + * @info: The ETableSortInfo object + * + * Returns: the number of sorting criteria in the object. + */ +guint +e_table_sort_info_sorting_get_count (ETableSortInfo *info) +{ + return info->sort_count; +} + +static void +e_table_sort_info_sorting_real_truncate (ETableSortInfo *info, + gint length) +{ + if (length < info->sort_count) { + info->sort_count = length; + } + if (length > info->sort_count) { + info->sortings = g_realloc (info->sortings, length * sizeof (ETableSortColumn)); + info->sort_count = length; + } +} + +/** + * e_table_sort_info_sorting_truncate: + * @info: The ETableSortInfo object + * @lenght: position where the truncation happens. + * + * This routine can be used to reduce or grow the number of sort + * criteria in the object. + */ +void +e_table_sort_info_sorting_truncate (ETableSortInfo *info, + gint length) +{ + e_table_sort_info_sorting_real_truncate (info, length); + e_table_sort_info_sort_info_changed (info); +} + +/** + * e_table_sort_info_sorting_get_nth: + * @info: The ETableSortInfo object + * @n: Item information to fetch. + * + * Returns: the description of the @n-th grouping criteria in the @info object. + */ +ETableSortColumn +e_table_sort_info_sorting_get_nth (ETableSortInfo *info, + gint n) +{ + if (n < info->sort_count) { + return info->sortings[n]; + } else { + ETableSortColumn fake = {0, 0}; + return fake; + } +} + +/** + * e_table_sort_info_sorting_get_nth: + * @info: The ETableSortInfo object + * @n: Item information to fetch. + * @column: new values for the sorting + * + * Sets the sorting criteria for index @n to be given by @column (a + * column number and whether it is ascending or descending). + */ +void +e_table_sort_info_sorting_set_nth (ETableSortInfo *info, + gint n, + ETableSortColumn column) +{ + if (n >= info->sort_count) { + e_table_sort_info_sorting_real_truncate (info, n + 1); + } + info->sortings[n] = column; + e_table_sort_info_sort_info_changed (info); +} + +/** + * e_table_sort_info_new: + * + * This creates a new e_table_sort_info object that contains no + * grouping and no sorting defined as of yet. This object is used + * to keep track of multi-level sorting and multi-level grouping of + * the ETable. + * + * Returns: A new %ETableSortInfo object + */ +ETableSortInfo * +e_table_sort_info_new (void) +{ + return g_object_new (E_TYPE_TABLE_SORT_INFO, NULL); +} + +/** + * e_table_sort_info_load_from_node: + * @info: The ETableSortInfo object + * @node: pointer to the xmlNode that describes the sorting and grouping information + * @state_version: + * + * This loads the state for the %ETableSortInfo object @info from the + * xml node @node. + */ +void +e_table_sort_info_load_from_node (ETableSortInfo *info, + xmlNode *node, + gdouble state_version) +{ + gint i; + xmlNode *grouping; + + if (state_version <= 0.05) { + i = 0; + for (grouping = node->xmlChildrenNode; grouping && !strcmp ((gchar *) grouping->name, "group"); grouping = grouping->xmlChildrenNode) { + ETableSortColumn column; + column.column = e_xml_get_integer_prop_by_name (grouping, (const guchar *)"column"); + column.ascending = e_xml_get_bool_prop_by_name (grouping, (const guchar *)"ascending"); + e_table_sort_info_grouping_set_nth (info, i++, column); + } + i = 0; + for (; grouping && !strcmp ((gchar *) grouping->name, "leaf"); grouping = grouping->xmlChildrenNode) { + ETableSortColumn column; + column.column = e_xml_get_integer_prop_by_name (grouping, (const guchar *)"column"); + column.ascending = e_xml_get_bool_prop_by_name (grouping, (const guchar *)"ascending"); + e_table_sort_info_sorting_set_nth (info, i++, column); + } + } else { + gint gcnt = 0; + gint scnt = 0; + for (grouping = node->children; grouping; grouping = grouping->next) { + ETableSortColumn column; + + if (grouping->type != XML_ELEMENT_NODE) + continue; + + if (!strcmp ((gchar *) grouping->name, "group")) { + column.column = e_xml_get_integer_prop_by_name (grouping, (const guchar *)"column"); + column.ascending = e_xml_get_bool_prop_by_name (grouping, (const guchar *)"ascending"); + e_table_sort_info_grouping_set_nth (info, gcnt++, column); + } else if (!strcmp ((gchar *) grouping->name, "leaf")) { + column.column = e_xml_get_integer_prop_by_name (grouping, (const guchar *)"column"); + column.ascending = e_xml_get_bool_prop_by_name (grouping, (const guchar *)"ascending"); + e_table_sort_info_sorting_set_nth (info, scnt++, column); + } + } + } + g_signal_emit (info, e_table_sort_info_signals[SORT_INFO_CHANGED], 0); +} + +/** + * e_table_sort_info_save_to_node: + * @info: The ETableSortInfo object + * @parent: xmlNode that will be hosting the saved state of the @info object. + * + * This function is used + * + * Returns: the node that has been appended to @parent as a child containing + * the sorting and grouping information for this ETableSortInfo object. + */ +xmlNode * +e_table_sort_info_save_to_node (ETableSortInfo *info, + xmlNode *parent) +{ + xmlNode *grouping; + gint i; + const gint sort_count = e_table_sort_info_sorting_get_count (info); + const gint group_count = e_table_sort_info_grouping_get_count (info); + + grouping = xmlNewChild (parent, NULL, (const guchar *)"grouping", NULL); + + for (i = 0; i < group_count; i++) { + ETableSortColumn column = e_table_sort_info_grouping_get_nth (info, i); + xmlNode *new_node = xmlNewChild (grouping, NULL, (const guchar *)"group", NULL); + + e_xml_set_integer_prop_by_name (new_node, (const guchar *)"column", column.column); + e_xml_set_bool_prop_by_name (new_node, (const guchar *)"ascending", column.ascending); + } + + for (i = 0; i < sort_count; i++) { + ETableSortColumn column = e_table_sort_info_sorting_get_nth (info, i); + xmlNode *new_node = xmlNewChild (grouping, NULL, (const guchar *)"leaf", NULL); + + e_xml_set_integer_prop_by_name (new_node, (const guchar *)"column", column.column); + e_xml_set_bool_prop_by_name (new_node, (const guchar *)"ascending", column.ascending); + } + + return grouping; +} + +ETableSortInfo * +e_table_sort_info_duplicate (ETableSortInfo *info) +{ + ETableSortInfo *new_info; + + new_info = e_table_sort_info_new (); + + new_info->group_count = info->group_count; + new_info->groupings = g_new (ETableSortColumn, new_info->group_count); + memmove (new_info->groupings, info->groupings, sizeof (ETableSortColumn) * new_info->group_count); + + new_info->sort_count = info->sort_count; + new_info->sortings = g_new (ETableSortColumn, new_info->sort_count); + memmove (new_info->sortings, info->sortings, sizeof (ETableSortColumn) * new_info->sort_count); + + new_info->can_group = info->can_group; + + return new_info; +} + +void +e_table_sort_info_set_can_group (ETableSortInfo *info, + gboolean can_group) +{ + info->can_group = can_group; +} + +gboolean +e_table_sort_info_get_can_group (ETableSortInfo *info) +{ + return info->can_group; +} + diff --git a/e-util/e-table-sort-info.h b/e-util/e-table-sort-info.h new file mode 100644 index 0000000000..c56c5b07f5 --- /dev/null +++ b/e-util/e-table-sort-info.h @@ -0,0 +1,133 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SORT_INFO_H_ +#define _E_TABLE_SORT_INFO_H_ + +#include <glib-object.h> +#include <libxml/tree.h> + +#define E_TYPE_TABLE_SORT_INFO \ + (e_table_sort_info_get_type ()) +#define E_TABLE_SORT_INFO(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_SORT_INFO, ETableSortInfo)) +#define E_TABLE_SORT_INFO_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_SORT_INFO, ETableSortInfoClass)) +#define E_IS_TABLE_SORT_INFO(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_SORT_INFO)) +#define E_IS_TABLE_SORT_INFO_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_SORT_INFO)) +#define E_TABLE_SORT_INFO_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_SORT_INFO, ETableSortInfoClass)) + +G_BEGIN_DECLS + +typedef struct _ETableSortColumn ETableSortColumn; + +typedef struct _ETableSortInfo ETableSortInfo; +typedef struct _ETableSortInfoClass ETableSortInfoClass; + +struct _ETableSortColumn { + guint column : 31; + guint ascending : 1; +}; + +struct _ETableSortInfo { + GObject parent; + + gint group_count; + ETableSortColumn *groupings; + gint sort_count; + ETableSortColumn *sortings; + + guint frozen : 1; + guint sort_info_changed : 1; + guint group_info_changed : 1; + + guint can_group : 1; +}; + +struct _ETableSortInfoClass { + GObjectClass parent_class; + + /* Signals */ + void (*sort_info_changed) (ETableSortInfo *info); + void (*group_info_changed) (ETableSortInfo *info); +}; + +GType e_table_sort_info_get_type (void) G_GNUC_CONST; + +void e_table_sort_info_freeze (ETableSortInfo *info); +void e_table_sort_info_thaw (ETableSortInfo *info); + +guint e_table_sort_info_grouping_get_count + (ETableSortInfo *info); +void e_table_sort_info_grouping_truncate + (ETableSortInfo *info, + gint length); +ETableSortColumn + e_table_sort_info_grouping_get_nth + (ETableSortInfo *info, + gint n); +void e_table_sort_info_grouping_set_nth + (ETableSortInfo *info, + gint n, + ETableSortColumn column); + +guint e_table_sort_info_sorting_get_count + (ETableSortInfo *info); +void e_table_sort_info_sorting_truncate + (ETableSortInfo *info, + gint length); +ETableSortColumn + e_table_sort_info_sorting_get_nth + (ETableSortInfo *info, + gint n); +void e_table_sort_info_sorting_set_nth + (ETableSortInfo *info, + gint n, + ETableSortColumn column); + +ETableSortInfo *e_table_sort_info_new (void); +void e_table_sort_info_load_from_node + (ETableSortInfo *info, + xmlNode *node, + gdouble state_version); +xmlNode * e_table_sort_info_save_to_node (ETableSortInfo *info, + xmlNode *parent); +ETableSortInfo *e_table_sort_info_duplicate (ETableSortInfo *info); +void e_table_sort_info_set_can_group (ETableSortInfo *info, + gboolean can_group); +gboolean e_table_sort_info_get_can_group (ETableSortInfo *info); + +G_END_DECLS + +#endif /* _E_TABLE_SORT_INFO_H_ */ diff --git a/e-util/e-table-sorted-variable.c b/e-util/e-table-sorted-variable.c new file mode 100644 index 0000000000..17c10d5328 --- /dev/null +++ b/e-util/e-table-sorted-variable.c @@ -0,0 +1,235 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <string.h> + +#include "e-table-sorted-variable.h" +#include "e-table-sorting-utils.h" + +#define d(x) + +#define INCREMENT_AMOUNT 100 + +/* maximum insertions between an idle event that we will do without scheduling an idle sort */ +#define ETSV_INSERT_MAX (4) + +/* workaround for avoiding API breakage */ +#define etsv_get_type e_table_sorted_variable_get_type +G_DEFINE_TYPE (ETableSortedVariable, etsv, E_TYPE_TABLE_SUBSET_VARIABLE) + +static void etsv_sort_info_changed (ETableSortInfo *info, ETableSortedVariable *etsv); +static void etsv_sort (ETableSortedVariable *etsv); +static void etsv_add (ETableSubsetVariable *etssv, gint row); +static void etsv_add_all (ETableSubsetVariable *etssv); + +static void +etsv_dispose (GObject *object) +{ + ETableSortedVariable *etsv = E_TABLE_SORTED_VARIABLE (object); + + if (etsv->sort_info_changed_id) + g_signal_handler_disconnect ( + etsv->sort_info, + etsv->sort_info_changed_id); + etsv->sort_info_changed_id = 0; + + if (etsv->sort_idle_id) { + g_source_remove (etsv->sort_idle_id); + etsv->sort_idle_id = 0; + } + if (etsv->insert_idle_id) { + g_source_remove (etsv->insert_idle_id); + etsv->insert_idle_id = 0; + } + + if (etsv->sort_info) + g_object_unref (etsv->sort_info); + etsv->sort_info = NULL; + + if (etsv->full_header) + g_object_unref (etsv->full_header); + etsv->full_header = NULL; + + G_OBJECT_CLASS (etsv_parent_class)->dispose (object); +} + +static void +etsv_class_init (ETableSortedVariableClass *class) +{ + ETableSubsetVariableClass *etssv_class = E_TABLE_SUBSET_VARIABLE_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = etsv_dispose; + + etssv_class->add = etsv_add; + etssv_class->add_all = etsv_add_all; +} + +static void +etsv_init (ETableSortedVariable *etsv) +{ + etsv->full_header = NULL; + etsv->sort_info = NULL; + + etsv->sort_info_changed_id = 0; + + etsv->sort_idle_id = 0; + etsv->insert_count = 0; +} + +static gboolean +etsv_sort_idle (ETableSortedVariable *etsv) +{ + g_object_ref (etsv); + etsv_sort (etsv); + etsv->sort_idle_id = 0; + etsv->insert_count = 0; + g_object_unref (etsv); + return FALSE; +} + +static gboolean +etsv_insert_idle (ETableSortedVariable *etsv) +{ + etsv->insert_count = 0; + etsv->insert_idle_id = 0; + return FALSE; +} + +static void +etsv_add (ETableSubsetVariable *etssv, + gint row) +{ + ETableModel *etm = E_TABLE_MODEL (etssv); + ETableSubset *etss = E_TABLE_SUBSET (etssv); + ETableSortedVariable *etsv = E_TABLE_SORTED_VARIABLE (etssv); + gint i; + + e_table_model_pre_change (etm); + + if (etss->n_map + 1 > etssv->n_vals_allocated) { + etssv->n_vals_allocated += INCREMENT_AMOUNT; + etss->map_table = g_realloc (etss->map_table, (etssv->n_vals_allocated) * sizeof (gint)); + } + i = etss->n_map; + if (etsv->sort_idle_id == 0) { + /* this is to see if we're inserting a lot of things between idle loops. + * If we are, we're busy, its faster to just append and perform a full sort later */ + etsv->insert_count++; + if (etsv->insert_count > ETSV_INSERT_MAX) { + /* schedule a sort, and append instead */ + etsv->sort_idle_id = g_idle_add_full (50, (GSourceFunc) etsv_sort_idle, etsv, NULL); + } else { + /* make sure we have an idle handler to reset the count every now and then */ + if (etsv->insert_idle_id == 0) { + etsv->insert_idle_id = g_idle_add_full (40, (GSourceFunc) etsv_insert_idle, etsv, NULL); + } + i = e_table_sorting_utils_insert (etss->source, etsv->sort_info, etsv->full_header, etss->map_table, etss->n_map, row); + memmove (etss->map_table + i + 1, etss->map_table + i, (etss->n_map - i) * sizeof (gint)); + } + } + etss->map_table[i] = row; + etss->n_map++; + + e_table_model_row_inserted (etm, i); +} + +static void +etsv_add_all (ETableSubsetVariable *etssv) +{ + ETableModel *etm = E_TABLE_MODEL (etssv); + ETableSubset *etss = E_TABLE_SUBSET (etssv); + ETableSortedVariable *etsv = E_TABLE_SORTED_VARIABLE (etssv); + gint rows; + gint i; + + e_table_model_pre_change (etm); + + rows = e_table_model_row_count (etss->source); + + if (etss->n_map + rows > etssv->n_vals_allocated) { + etssv->n_vals_allocated += MAX (INCREMENT_AMOUNT, rows); + etss->map_table = g_realloc (etss->map_table, etssv->n_vals_allocated * sizeof (gint)); + } + for (i = 0; i < rows; i++) + etss->map_table[etss->n_map++] = i; + + if (etsv->sort_idle_id == 0) { + etsv->sort_idle_id = g_idle_add_full (50, (GSourceFunc) etsv_sort_idle, etsv, NULL); + } + + e_table_model_changed (etm); +} + +ETableModel * +e_table_sorted_variable_new (ETableModel *source, + ETableHeader *full_header, + ETableSortInfo *sort_info) +{ + ETableSortedVariable *etsv = g_object_new (E_TYPE_TABLE_SORTED_VARIABLE, NULL); + ETableSubsetVariable *etssv = E_TABLE_SUBSET_VARIABLE (etsv); + + if (e_table_subset_variable_construct (etssv, source) == NULL) { + g_object_unref (etsv); + return NULL; + } + + etsv->sort_info = sort_info; + g_object_ref (etsv->sort_info); + etsv->full_header = full_header; + g_object_ref (etsv->full_header); + + etsv->sort_info_changed_id = g_signal_connect ( + sort_info, "sort_info_changed", + G_CALLBACK (etsv_sort_info_changed), etsv); + + return E_TABLE_MODEL (etsv); +} + +static void +etsv_sort_info_changed (ETableSortInfo *info, + ETableSortedVariable *etsv) +{ + etsv_sort (etsv); +} + +static void +etsv_sort (ETableSortedVariable *etsv) +{ + ETableSubset *etss = E_TABLE_SUBSET (etsv); + static gint reentering = 0; + if (reentering) + return; + reentering = 1; + + e_table_model_pre_change (E_TABLE_MODEL (etsv)); + + e_table_sorting_utils_sort (etss->source, etsv->sort_info, etsv->full_header, etss->map_table, etss->n_map); + + e_table_model_changed (E_TABLE_MODEL (etsv)); + reentering = 0; +} diff --git a/e-util/e-table-sorted-variable.h b/e-util/e-table-sorted-variable.h new file mode 100644 index 0000000000..60861e527a --- /dev/null +++ b/e-util/e-table-sorted-variable.h @@ -0,0 +1,85 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SORTED_VARIABLE_H_ +#define _E_TABLE_SORTED_VARIABLE_H_ + +#include <e-util/e-table-header.h> +#include <e-util/e-table-model.h> +#include <e-util/e-table-sort-info.h> +#include <e-util/e-table-subset-variable.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_SORTED_VARIABLE \ + (e_table_sorted_variable_get_type ()) +#define E_TABLE_SORTED_VARIABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_SORTED_VARIABLE, ETableSortedVariable)) +#define E_TABLE_SORTED_VARIABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_SORTED_VARIABLE, ETableSortedVariableClass)) +#define E_IS_TABLE_SORTED_VARIABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_SORTED_VARIABLE)) +#define E_IS_TABLE_SORTED_VARIABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_SORTED_VARIABLE)) +#define E_TABLE_SORTED_VARIABLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_SORTED_VARIABLE, ETableSortedVariableClass)) + +G_BEGIN_DECLS + +typedef struct _ETableSortedVariable ETableSortedVariable; +typedef struct _ETableSortedVariableClass ETableSortedVariableClass; + +struct _ETableSortedVariable { + ETableSubsetVariable parent; + + ETableSortInfo *sort_info; + + ETableHeader *full_header; + + gint sort_info_changed_id; + gint sort_idle_id; + gint insert_idle_id; + gint insert_count; +}; + +struct _ETableSortedVariableClass { + ETableSubsetVariableClass parent_class; +}; + +GType e_table_sorted_variable_get_type + (void) G_GNUC_CONST; +ETableModel * e_table_sorted_variable_new (ETableModel *etm, + ETableHeader *header, + ETableSortInfo *sort_info); + +G_END_DECLS + +#endif /* _E_TABLE_SORTED_VARIABLE_H_ */ diff --git a/e-util/e-table-sorted.c b/e-util/e-table-sorted.c new file mode 100644 index 0000000000..3f548d349b --- /dev/null +++ b/e-util/e-table-sorted.c @@ -0,0 +1,328 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <string.h> + +#include "e-table-sorted.h" +#include "e-table-sorting-utils.h" + +#define d(x) + +#define INCREMENT_AMOUNT 100 + +/* workaround for avoding API breakage */ +#define ets_get_type e_table_sorted_get_type +G_DEFINE_TYPE (ETableSorted, ets, E_TYPE_TABLE_SUBSET) + +/* maximum insertions between an idle event that we will do without scheduling an idle sort */ +#define ETS_INSERT_MAX (4) + +static void ets_sort_info_changed (ETableSortInfo *info, ETableSorted *ets); +static void ets_sort (ETableSorted *ets); +static void ets_proxy_model_changed (ETableSubset *etss, ETableModel *source); +static void ets_proxy_model_row_changed (ETableSubset *etss, ETableModel *source, gint row); +static void ets_proxy_model_cell_changed (ETableSubset *etss, ETableModel *source, gint col, gint row); +static void ets_proxy_model_rows_inserted (ETableSubset *etss, ETableModel *source, gint row, gint count); +static void ets_proxy_model_rows_deleted (ETableSubset *etss, ETableModel *source, gint row, gint count); + +static void +ets_dispose (GObject *object) +{ + ETableSorted *ets = E_TABLE_SORTED (object); + + if (ets->sort_idle_id) + g_source_remove (ets->sort_idle_id); + ets->sort_idle_id = 0; + + if (ets->insert_idle_id) + g_source_remove (ets->insert_idle_id); + ets->insert_idle_id = 0; + + if (ets->sort_info) { + g_signal_handler_disconnect ( + ets->sort_info, + ets->sort_info_changed_id); + g_object_unref (ets->sort_info); + ets->sort_info = NULL; + } + + if (ets->full_header) + g_object_unref (ets->full_header); + ets->full_header = NULL; + + G_OBJECT_CLASS (ets_parent_class)->dispose (object); +} + +static void +ets_class_init (ETableSortedClass *class) +{ + ETableSubsetClass *etss_class = E_TABLE_SUBSET_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + etss_class->proxy_model_changed = ets_proxy_model_changed; + etss_class->proxy_model_row_changed = ets_proxy_model_row_changed; + etss_class->proxy_model_cell_changed = ets_proxy_model_cell_changed; + etss_class->proxy_model_rows_inserted = ets_proxy_model_rows_inserted; + etss_class->proxy_model_rows_deleted = ets_proxy_model_rows_deleted; + + object_class->dispose = ets_dispose; +} + +static void +ets_init (ETableSorted *ets) +{ + ets->full_header = NULL; + ets->sort_info = NULL; + + ets->sort_info_changed_id = 0; + + ets->sort_idle_id = 0; + ets->insert_count = 0; +} + +static gboolean +ets_sort_idle (ETableSorted *ets) +{ + g_object_ref (ets); + ets_sort (ets); + ets->sort_idle_id = 0; + ets->insert_count = 0; + g_object_unref (ets); + return FALSE; +} + +static gboolean +ets_insert_idle (ETableSorted *ets) +{ + ets->insert_count = 0; + ets->insert_idle_id = 0; + return FALSE; +} + +ETableModel * +e_table_sorted_new (ETableModel *source, + ETableHeader *full_header, + ETableSortInfo *sort_info) +{ + ETableSorted *ets = g_object_new (E_TYPE_TABLE_SORTED, NULL); + ETableSubset *etss = E_TABLE_SUBSET (ets); + + if (E_TABLE_SUBSET_CLASS (ets_parent_class)->proxy_model_pre_change) + (E_TABLE_SUBSET_CLASS (ets_parent_class)->proxy_model_pre_change) (etss, source); + + if (e_table_subset_construct (etss, source, 0) == NULL) { + g_object_unref (ets); + return NULL; + } + + ets->sort_info = sort_info; + g_object_ref (ets->sort_info); + ets->full_header = full_header; + g_object_ref (ets->full_header); + + ets_proxy_model_changed (etss, source); + + ets->sort_info_changed_id = g_signal_connect ( + sort_info, "sort_info_changed", + G_CALLBACK (ets_sort_info_changed), ets); + + return E_TABLE_MODEL (ets); +} + +static void +ets_sort_info_changed (ETableSortInfo *info, + ETableSorted *ets) +{ + ets_sort (ets); +} + +static void +ets_proxy_model_changed (ETableSubset *subset, + ETableModel *source) +{ + gint rows, i; + + rows = e_table_model_row_count (source); + + g_free (subset->map_table); + subset->n_map = rows; + subset->map_table = g_new (int, rows); + + for (i = 0; i < rows; i++) { + subset->map_table[i] = i; + } + + if (!E_TABLE_SORTED (subset)->sort_idle_id) + E_TABLE_SORTED (subset)->sort_idle_id = g_idle_add_full (50, (GSourceFunc) ets_sort_idle, subset, NULL); + + e_table_model_changed (E_TABLE_MODEL (subset)); +} + +static void +ets_proxy_model_row_changed (ETableSubset *subset, + ETableModel *source, + gint row) +{ + if (!E_TABLE_SORTED (subset)->sort_idle_id) + E_TABLE_SORTED (subset)->sort_idle_id = g_idle_add_full (50, (GSourceFunc) ets_sort_idle, subset, NULL); + + if (E_TABLE_SUBSET_CLASS (ets_parent_class)->proxy_model_row_changed) + (E_TABLE_SUBSET_CLASS (ets_parent_class)->proxy_model_row_changed) (subset, source, row); +} + +static void +ets_proxy_model_cell_changed (ETableSubset *subset, + ETableModel *source, + gint col, + gint row) +{ + ETableSorted *ets = E_TABLE_SORTED (subset); + if (e_table_sorting_utils_affects_sort (ets->sort_info, ets->full_header, col)) + ets_proxy_model_row_changed (subset, source, row); + else if (E_TABLE_SUBSET_CLASS (ets_parent_class)->proxy_model_cell_changed) + (E_TABLE_SUBSET_CLASS (ets_parent_class)->proxy_model_cell_changed) (subset, source, col, row); +} + +static void +ets_proxy_model_rows_inserted (ETableSubset *etss, + ETableModel *source, + gint row, + gint count) +{ + ETableModel *etm = E_TABLE_MODEL (etss); + ETableSorted *ets = E_TABLE_SORTED (etss); + gint i; + gboolean full_change = FALSE; + + if (count == 0) { + e_table_model_no_change (etm); + return; + } + + if (row != etss->n_map) { + full_change = TRUE; + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] >= row) { + etss->map_table[i] += count; + } + } + } + + etss->map_table = g_realloc (etss->map_table, (etss->n_map + count) * sizeof (gint)); + + for (; count > 0; count--) { + if (!full_change) + e_table_model_pre_change (etm); + i = etss->n_map; + if (ets->sort_idle_id == 0) { + /* this is to see if we're inserting a lot of things between idle loops. + * If we are, we're busy, its faster to just append and perform a full sort later */ + ets->insert_count++; + if (ets->insert_count > ETS_INSERT_MAX) { + /* schedule a sort, and append instead */ + ets->sort_idle_id = g_idle_add_full (50, (GSourceFunc) ets_sort_idle, ets, NULL); + } else { + /* make sure we have an idle handler to reset the count every now and then */ + if (ets->insert_idle_id == 0) { + ets->insert_idle_id = g_idle_add_full (40, (GSourceFunc) ets_insert_idle, ets, NULL); + } + i = e_table_sorting_utils_insert (etss->source, ets->sort_info, ets->full_header, etss->map_table, etss->n_map, row); + memmove (etss->map_table + i + 1, etss->map_table + i, (etss->n_map - i) * sizeof (gint)); + } + } + etss->map_table[i] = row; + etss->n_map++; + if (!full_change) { + e_table_model_row_inserted (etm, i); + } + + d (g_print ("inserted row %d", row)); + row++; + } + if (full_change) + e_table_model_changed (etm); + else + e_table_model_no_change (etm); + d (e_table_subset_print_debugging (etss)); +} + +static void +ets_proxy_model_rows_deleted (ETableSubset *etss, + ETableModel *source, + gint row, + gint count) +{ + ETableModel *etm = E_TABLE_MODEL (etss); + gint i; + gboolean shift; + gint j; + + shift = row == etss->n_map - count; + + for (j = 0; j < count; j++) { + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] == row + j) { + if (shift) + e_table_model_pre_change (etm); + memmove (etss->map_table + i, etss->map_table + i + 1, (etss->n_map - i - 1) * sizeof (gint)); + etss->n_map--; + if (shift) + e_table_model_row_deleted (etm, i); + } + } + } + if (!shift) { + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] >= row) + etss->map_table[i] -= count; + } + + e_table_model_changed (etm); + } else { + e_table_model_no_change (etm); + } + + d (g_print ("deleted row %d count %d", row, count)); + d (e_table_subset_print_debugging (etss)); +} + +static void +ets_sort (ETableSorted *ets) +{ + ETableSubset *etss = E_TABLE_SUBSET (ets); + static gint reentering = 0; + if (reentering) + return; + reentering = 1; + + e_table_model_pre_change (E_TABLE_MODEL (ets)); + + e_table_sorting_utils_sort (etss->source, ets->sort_info, ets->full_header, etss->map_table, etss->n_map); + + e_table_model_changed (E_TABLE_MODEL (ets)); + reentering = 0; +} diff --git a/e-util/e-table-sorted.h b/e-util/e-table-sorted.h new file mode 100644 index 0000000000..c9f4b65482 --- /dev/null +++ b/e-util/e-table-sorted.h @@ -0,0 +1,84 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SORTED_H_ +#define _E_TABLE_SORTED_H_ + +#include <e-util/e-table-header.h> +#include <e-util/e-table-model.h> +#include <e-util/e-table-sort-info.h> +#include <e-util/e-table-subset.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_SORTED \ + (e_table_sorted_get_type ()) +#define E_TABLE_SORTED(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_SORTED, ETableSorted)) +#define E_TABLE_SORTED_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_SORTED, ETableSortedClass)) +#define E_IS_TABLE_SORTED(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_SORTED)) +#define E_IS_TABLE_SORTED_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_SORTED)) +#define E_TABLE_SORTED_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_SORTED, ETableSortedClass)) + +G_BEGIN_DECLS + +typedef struct _ETableSorted ETableSorted; +typedef struct _ETableSortedClass ETableSortedClass; + +struct _ETableSorted { + ETableSubset parent; + + ETableSortInfo *sort_info; + + ETableHeader *full_header; + + gint sort_info_changed_id; + gint sort_idle_id; + gint insert_idle_id; + gint insert_count; +}; + +struct _ETableSortedClass { + ETableSubsetClass parent_class; +}; + +GType e_table_sorted_get_type (void) G_GNUC_CONST; +ETableModel * e_table_sorted_new (ETableModel *etm, + ETableHeader *header, + ETableSortInfo *sort_info); + +G_END_DECLS + +#endif /* _E_TABLE_SORTED_H_ */ diff --git a/e-util/e-table-sorter.c b/e-util/e-table-sorter.c new file mode 100644 index 0000000000..5fdc077503 --- /dev/null +++ b/e-util/e-table-sorter.c @@ -0,0 +1,519 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <string.h> + +#include <glib/gi18n.h> + +#include "e-table-sorter.h" +#include "e-table-sorting-utils.h" + +#define d(x) + +enum { + PROP_0, + PROP_SORT_INFO +}; + +/* workaround for avoiding API breakage */ +#define ets_get_type e_table_sorter_get_type +G_DEFINE_TYPE (ETableSorter, ets, E_SORTER_TYPE) + +#define INCREMENT_AMOUNT 100 + +static void ets_model_changed (ETableModel *etm, ETableSorter *ets); +static void ets_model_row_changed (ETableModel *etm, gint row, ETableSorter *ets); +static void ets_model_cell_changed (ETableModel *etm, gint col, gint row, ETableSorter *ets); +static void ets_model_rows_inserted (ETableModel *etm, gint row, gint count, ETableSorter *ets); +static void ets_model_rows_deleted (ETableModel *etm, gint row, gint count, ETableSorter *ets); +static void ets_sort_info_changed (ETableSortInfo *info, ETableSorter *ets); +static void ets_clean (ETableSorter *ets); +static void ets_sort (ETableSorter *ets); +static void ets_backsort (ETableSorter *ets); + +static gint ets_model_to_sorted (ESorter *sorter, gint row); +static gint ets_sorted_to_model (ESorter *sorter, gint row); +static void ets_get_model_to_sorted_array (ESorter *sorter, gint **array, gint *count); +static void ets_get_sorted_to_model_array (ESorter *sorter, gint **array, gint *count); +static gboolean ets_needs_sorting (ESorter *ets); + +static void +ets_dispose (GObject *object) +{ + ETableSorter *ets = E_TABLE_SORTER (object); + + if (ets->sort_info) { + if (ets->table_model_changed_id) + g_signal_handler_disconnect ( + ets->source, + ets->table_model_changed_id); + if (ets->table_model_row_changed_id) + g_signal_handler_disconnect ( + ets->source, + ets->table_model_row_changed_id); + if (ets->table_model_cell_changed_id) + g_signal_handler_disconnect ( + ets->source, + ets->table_model_cell_changed_id); + if (ets->table_model_rows_inserted_id) + g_signal_handler_disconnect ( + ets->source, + ets->table_model_rows_inserted_id); + if (ets->table_model_rows_deleted_id) + g_signal_handler_disconnect ( + ets->source, + ets->table_model_rows_deleted_id); + if (ets->sort_info_changed_id) + g_signal_handler_disconnect ( + ets->sort_info, + ets->sort_info_changed_id); + if (ets->group_info_changed_id) + g_signal_handler_disconnect ( + ets->sort_info, + ets->group_info_changed_id); + + ets->table_model_changed_id = 0; + ets->table_model_row_changed_id = 0; + ets->table_model_cell_changed_id = 0; + ets->table_model_rows_inserted_id = 0; + ets->table_model_rows_deleted_id = 0; + ets->sort_info_changed_id = 0; + ets->group_info_changed_id = 0; + + g_object_unref (ets->sort_info); + ets->sort_info = NULL; + } + + if (ets->full_header) + g_object_unref (ets->full_header); + ets->full_header = NULL; + + if (ets->source) + g_object_unref (ets->source); + ets->source = NULL; + + G_OBJECT_CLASS (ets_parent_class)->dispose (object); +} + +static void +ets_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ETableSorter *ets = E_TABLE_SORTER (object); + + switch (property_id) { + case PROP_SORT_INFO: + if (ets->sort_info) { + if (ets->sort_info_changed_id) + g_signal_handler_disconnect (ets->sort_info, ets->sort_info_changed_id); + if (ets->group_info_changed_id) + g_signal_handler_disconnect (ets->sort_info, ets->group_info_changed_id); + g_object_unref (ets->sort_info); + } + + ets->sort_info = E_TABLE_SORT_INFO (g_value_get_object (value)); + g_object_ref (ets->sort_info); + ets->sort_info_changed_id = g_signal_connect ( + ets->sort_info, "sort_info_changed", + G_CALLBACK (ets_sort_info_changed), ets); + ets->group_info_changed_id = g_signal_connect ( + ets->sort_info, "group_info_changed", + G_CALLBACK (ets_sort_info_changed), ets); + + ets_clean (ets); + break; + default: + break; + } +} + +static void +ets_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETableSorter *ets = E_TABLE_SORTER (object); + switch (property_id) { + case PROP_SORT_INFO: + g_value_set_object (value, ets->sort_info); + break; + } +} + +static void +ets_class_init (ETableSorterClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + ESorterClass *sorter_class = E_SORTER_CLASS (class); + + object_class->dispose = ets_dispose; + object_class->set_property = ets_set_property; + object_class->get_property = ets_get_property; + + sorter_class->model_to_sorted = ets_model_to_sorted; + sorter_class->sorted_to_model = ets_sorted_to_model; + sorter_class->get_model_to_sorted_array = ets_get_model_to_sorted_array; + sorter_class->get_sorted_to_model_array = ets_get_sorted_to_model_array; + sorter_class->needs_sorting = ets_needs_sorting; + + g_object_class_install_property ( + object_class, + PROP_SORT_INFO, + g_param_spec_object ( + "sort_info", + "Sort Info", + NULL, + E_TYPE_TABLE_SORT_INFO, + G_PARAM_READWRITE)); +} + +static void +ets_init (ETableSorter *ets) +{ + ets->full_header = NULL; + ets->sort_info = NULL; + ets->source = NULL; + + ets->needs_sorting = -1; + + ets->table_model_changed_id = 0; + ets->table_model_row_changed_id = 0; + ets->table_model_cell_changed_id = 0; + ets->table_model_rows_inserted_id = 0; + ets->table_model_rows_deleted_id = 0; + ets->sort_info_changed_id = 0; + ets->group_info_changed_id = 0; +} + +ETableSorter * +e_table_sorter_new (ETableModel *source, + ETableHeader *full_header, + ETableSortInfo *sort_info) +{ + ETableSorter *ets = g_object_new (E_TYPE_TABLE_SORTER, NULL); + + ets->sort_info = sort_info; + g_object_ref (ets->sort_info); + ets->full_header = full_header; + g_object_ref (ets->full_header); + ets->source = source; + g_object_ref (ets->source); + + ets->table_model_changed_id = g_signal_connect ( + source, "model_changed", + G_CALLBACK (ets_model_changed), ets); + + ets->table_model_row_changed_id = g_signal_connect ( + source, "model_row_changed", + G_CALLBACK (ets_model_row_changed), ets); + + ets->table_model_cell_changed_id = g_signal_connect ( + source, "model_cell_changed", + G_CALLBACK (ets_model_cell_changed), ets); + + ets->table_model_rows_inserted_id = g_signal_connect ( + source, "model_rows_inserted", + G_CALLBACK (ets_model_rows_inserted), ets); + + ets->table_model_rows_deleted_id = g_signal_connect ( + source, "model_rows_deleted", + G_CALLBACK (ets_model_rows_deleted), ets); + + ets->sort_info_changed_id = g_signal_connect ( + sort_info, "sort_info_changed", + G_CALLBACK (ets_sort_info_changed), ets); + + ets->group_info_changed_id = g_signal_connect ( + sort_info, "group_info_changed", + G_CALLBACK (ets_sort_info_changed), ets); + + return ets; +} + +static void +ets_model_changed (ETableModel *etm, + ETableSorter *ets) +{ + ets_clean (ets); +} + +static void +ets_model_row_changed (ETableModel *etm, + gint row, + ETableSorter *ets) +{ + ets_clean (ets); +} + +static void +ets_model_cell_changed (ETableModel *etm, + gint col, + gint row, + ETableSorter *ets) +{ + ets_clean (ets); +} + +static void +ets_model_rows_inserted (ETableModel *etm, + gint row, + gint count, + ETableSorter *ets) +{ + ets_clean (ets); +} + +static void +ets_model_rows_deleted (ETableModel *etm, + gint row, + gint count, + ETableSorter *ets) +{ + ets_clean (ets); +} + +static void +ets_sort_info_changed (ETableSortInfo *info, + ETableSorter *ets) +{ + d (g_print ("sort info changed\n")); + ets_clean (ets); +} + +struct qsort_data { + ETableSorter *ets; + gpointer *vals; + gint cols; + gint *ascending; + GCompareDataFunc *compare; + gpointer cmp_cache; +}; + +/* FIXME: Make it not cache the second and later columns (as if anyone cares.) */ + +static gint +qsort_callback (gconstpointer data1, + gconstpointer data2, + gpointer user_data) +{ + struct qsort_data *qd = (struct qsort_data *) user_data; + gint row1 = *(gint *) data1; + gint row2 = *(gint *) data2; + gint j; + gint sort_count = e_table_sort_info_sorting_get_count (qd->ets->sort_info) + e_table_sort_info_grouping_get_count (qd->ets->sort_info); + gint comp_val = 0; + gint ascending = 1; + for (j = 0; j < sort_count; j++) { + comp_val = (*(qd->compare[j]))(qd->vals[qd->cols * row1 + j], qd->vals[qd->cols * row2 + j], qd->cmp_cache); + ascending = qd->ascending[j]; + if (comp_val != 0) + break; + } + if (comp_val == 0) { + if (row1 < row2) + comp_val = -1; + if (row1 > row2) + comp_val = 1; + } + if (!ascending) + comp_val = -comp_val; + return comp_val; +} + +static void +ets_clean (ETableSorter *ets) +{ + g_free (ets->sorted); + ets->sorted = NULL; + + g_free (ets->backsorted); + ets->backsorted = NULL; + + ets->needs_sorting = -1; +} + +static void +ets_sort (ETableSorter *ets) +{ + gint rows; + gint i; + gint j; + gint cols; + gint group_cols; + struct qsort_data qd; + + if (ets->sorted) + return; + + rows = e_table_model_row_count (ets->source); + group_cols = e_table_sort_info_grouping_get_count (ets->sort_info); + cols = e_table_sort_info_sorting_get_count (ets->sort_info) + group_cols; + + ets->sorted = g_new (int, rows); + for (i = 0; i < rows; i++) + ets->sorted[i] = i; + + qd.cols = cols; + qd.ets = ets; + + qd.vals = g_new (gpointer , rows * cols); + qd.ascending = g_new (int, cols); + qd.compare = g_new (GCompareDataFunc, cols); + qd.cmp_cache = e_table_sorting_utils_create_cmp_cache (); + + for (j = 0; j < cols; j++) { + ETableSortColumn column; + ETableCol *col; + + if (j < group_cols) + column = e_table_sort_info_grouping_get_nth (ets->sort_info, j); + else + column = e_table_sort_info_sorting_get_nth (ets->sort_info, j - group_cols); + + col = e_table_header_get_column_by_col_idx (ets->full_header, column.column); + if (col == NULL) + col = e_table_header_get_column (ets->full_header, e_table_header_count (ets->full_header) - 1); + + for (i = 0; i < rows; i++) { + qd.vals[i * cols + j] = e_table_model_value_at (ets->source, col->col_idx, i); + } + + qd.compare[j] = col->compare; + qd.ascending[j] = column.ascending; + } + + g_qsort_with_data (ets->sorted, rows, sizeof (gint), qsort_callback, &qd); + + g_free (qd.vals); + g_free (qd.ascending); + g_free (qd.compare); + e_table_sorting_utils_free_cmp_cache (qd.cmp_cache); +} + +static void +ets_backsort (ETableSorter *ets) +{ + gint i, rows; + + if (ets->backsorted) + return; + + ets_sort (ets); + + rows = e_table_model_row_count (ets->source); + ets->backsorted = g_new0 (int, rows); + + for (i = 0; i < rows; i++) { + ets->backsorted[ets->sorted[i]] = i; + } +} + +static gint +ets_model_to_sorted (ESorter *es, + gint row) +{ + ETableSorter *ets = E_TABLE_SORTER (es); + gint rows = e_table_model_row_count (ets->source); + + g_return_val_if_fail (row >= 0, -1); + g_return_val_if_fail (row < rows, -1); + + if (ets_needs_sorting (es)) + ets_backsort (ets); + + if (ets->backsorted) + return ets->backsorted[row]; + else + return row; +} + +static gint +ets_sorted_to_model (ESorter *es, + gint row) +{ + ETableSorter *ets = E_TABLE_SORTER (es); + gint rows = e_table_model_row_count (ets->source); + + g_return_val_if_fail (row >= 0, -1); + g_return_val_if_fail (row < rows, -1); + + if (ets_needs_sorting (es)) + ets_sort (ets); + + if (ets->sorted) + return ets->sorted[row]; + else + return row; +} + +static void +ets_get_model_to_sorted_array (ESorter *es, + gint **array, + gint *count) +{ + ETableSorter *ets = E_TABLE_SORTER (es); + if (array || count) { + ets_backsort (ets); + + if (array) + *array = ets->backsorted; + if (count) + *count = e_table_model_row_count(ets->source); + } +} + +static void +ets_get_sorted_to_model_array (ESorter *es, + gint **array, + gint *count) +{ + ETableSorter *ets = E_TABLE_SORTER (es); + if (array || count) { + ets_sort (ets); + + if (array) + *array = ets->sorted; + if (count) + *count = e_table_model_row_count(ets->source); + } +} + +static gboolean +ets_needs_sorting (ESorter *es) +{ + ETableSorter *ets = E_TABLE_SORTER (es); + if (ets->needs_sorting < 0) { + if (e_table_sort_info_sorting_get_count (ets->sort_info) + e_table_sort_info_grouping_get_count (ets->sort_info)) + ets->needs_sorting = 1; + else + ets->needs_sorting = 0; + } + return ets->needs_sorting; +} diff --git a/e-util/e-table-sorter.h b/e-util/e-table-sorter.h new file mode 100644 index 0000000000..9615a9b17f --- /dev/null +++ b/e-util/e-table-sorter.h @@ -0,0 +1,94 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SORTER_H_ +#define _E_TABLE_SORTER_H_ + +#include <e-util/e-sorter.h> +#include <e-util/e-table-header.h> +#include <e-util/e-table-model.h> +#include <e-util/e-table-sort-info.h> +#include <e-util/e-table-subset-variable.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_SORTER \ + (e_table_sorter_get_type ()) +#define E_TABLE_SORTER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_SORTER, ETableSorter)) +#define E_TABLE_SORTER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_SORTER, ETableSorterClass)) +#define E_IS_TABLE_SORTER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_SORTER)) +#define E_IS_TABLE_SORTER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_SORTER)) +#define E_TABLE_SORTER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_SORTER, ETableSorterClass)) + +G_BEGIN_DECLS + +typedef struct _ETableSorter ETableSorter; +typedef struct _ETableSorterClass ETableSorterClass; + +struct _ETableSorter { + ESorter parent; + + ETableModel *source; + ETableHeader *full_header; + ETableSortInfo *sort_info; + + /* If needs_sorting is 0, then model_to_sorted + * and sorted_to_model are no-ops. */ + gint needs_sorting; + + gint *sorted; + gint *backsorted; + + gint table_model_changed_id; + gint table_model_row_changed_id; + gint table_model_cell_changed_id; + gint table_model_rows_inserted_id; + gint table_model_rows_deleted_id; + gint sort_info_changed_id; + gint group_info_changed_id; +}; + +struct _ETableSorterClass { + ESorterClass parent_class; +}; + +GType e_table_sorter_get_type (void) G_GNUC_CONST; +ETableSorter * e_table_sorter_new (ETableModel *etm, + ETableHeader *full_header, + ETableSortInfo *sort_info); + +G_END_DECLS + +#endif /* _E_TABLE_SORTER_H_ */ diff --git a/e-util/e-table-sorting-utils.c b/e-util/e-table-sorting-utils.c new file mode 100644 index 0000000000..23303ea418 --- /dev/null +++ b/e-util/e-table-sorting-utils.c @@ -0,0 +1,492 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-sorting-utils.h" + +#include <string.h> +#include <camel/camel.h> + +#include "e-misc-utils.h" + +#define d(x) + +/* This takes source rows. */ +static gint +etsu_compare (ETableModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + gint row1, + gint row2, + gpointer cmp_cache) +{ + gint j; + gint sort_count = e_table_sort_info_sorting_get_count (sort_info); + gint comp_val = 0; + gint ascending = 1; + + for (j = 0; j < sort_count; j++) { + ETableSortColumn column = e_table_sort_info_sorting_get_nth (sort_info, j); + ETableCol *col; + col = e_table_header_get_column_by_col_idx (full_header, column.column); + if (col == NULL) + col = e_table_header_get_column (full_header, e_table_header_count (full_header) - 1); + comp_val = (*col->compare)(e_table_model_value_at (source, col->compare_col, row1), + e_table_model_value_at (source, col->compare_col, row2), + cmp_cache); + ascending = column.ascending; + if (comp_val != 0) + break; + } + if (comp_val == 0) { + if (row1 < row2) + comp_val = -1; + if (row1 > row2) + comp_val = 1; + } + if (!ascending) + comp_val = -comp_val; + return comp_val; +} + +typedef struct { + gint cols; + gpointer *vals; + gint *ascending; + GCompareDataFunc *compare; + gpointer cmp_cache; +} ETableSortClosure; + +typedef struct { + ETreeModel *tree; + ETableSortInfo *sort_info; + ETableHeader *full_header; + gpointer cmp_cache; +} ETreeSortClosure; + +/* FIXME: Make it not cache the second and later columns (as if anyone cares.) */ + +static gint +e_sort_callback (gconstpointer data1, + gconstpointer data2, + gpointer user_data) +{ + gint row1 = *(gint *) data1; + gint row2 = *(gint *) data2; + ETableSortClosure *closure = user_data; + gint j; + gint sort_count = closure->cols; + gint comp_val = 0; + gint ascending = 1; + for (j = 0; j < sort_count; j++) { + comp_val = (*(closure->compare[j]))(closure->vals[closure->cols * row1 + j], closure->vals[closure->cols * row2 + j], closure->cmp_cache); + ascending = closure->ascending[j]; + if (comp_val != 0) + break; + } + if (comp_val == 0) { + if (row1 < row2) + comp_val = -1; + if (row1 > row2) + comp_val = 1; + } + if (!ascending) + comp_val = -comp_val; + return comp_val; +} + +void +e_table_sorting_utils_sort (ETableModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + gint *map_table, + gint rows) +{ + gint total_rows; + gint i; + gint j; + gint cols; + ETableSortClosure closure; + + g_return_if_fail (source != NULL); + g_return_if_fail (E_IS_TABLE_MODEL (source)); + g_return_if_fail (sort_info != NULL); + g_return_if_fail (E_IS_TABLE_SORT_INFO (sort_info)); + g_return_if_fail (full_header != NULL); + g_return_if_fail (E_IS_TABLE_HEADER (full_header)); + + total_rows = e_table_model_row_count (source); + cols = e_table_sort_info_sorting_get_count (sort_info); + closure.cols = cols; + + closure.vals = g_new (gpointer , total_rows * cols); + closure.ascending = g_new (int, cols); + closure.compare = g_new (GCompareDataFunc, cols); + closure.cmp_cache = e_table_sorting_utils_create_cmp_cache (); + + for (j = 0; j < cols; j++) { + ETableSortColumn column = e_table_sort_info_sorting_get_nth (sort_info, j); + ETableCol *col; + col = e_table_header_get_column_by_col_idx (full_header, column.column); + if (col == NULL) + col = e_table_header_get_column (full_header, e_table_header_count (full_header) - 1); + for (i = 0; i < rows; i++) { + closure.vals[map_table[i] * cols + j] = e_table_model_value_at (source, col->compare_col, map_table[i]); + } + closure.compare[j] = col->compare; + closure.ascending[j] = column.ascending; + } + + g_qsort_with_data ( + map_table, rows, sizeof (gint), e_sort_callback, &closure); + + g_free (closure.vals); + g_free (closure.ascending); + g_free (closure.compare); + e_table_sorting_utils_free_cmp_cache (closure.cmp_cache); +} + +gboolean +e_table_sorting_utils_affects_sort (ETableSortInfo *sort_info, + ETableHeader *full_header, + gint col) +{ + gint j; + gint cols; + + g_return_val_if_fail (sort_info != NULL, TRUE); + g_return_val_if_fail (E_IS_TABLE_SORT_INFO (sort_info), TRUE); + g_return_val_if_fail (full_header != NULL, TRUE); + g_return_val_if_fail (E_IS_TABLE_HEADER (full_header), TRUE); + + cols = e_table_sort_info_sorting_get_count (sort_info); + + for (j = 0; j < cols; j++) { + ETableSortColumn column = e_table_sort_info_sorting_get_nth (sort_info, j); + ETableCol *tablecol; + tablecol = e_table_header_get_column_by_col_idx (full_header, column.column); + if (tablecol == NULL) + tablecol = e_table_header_get_column (full_header, e_table_header_count (full_header) - 1); + if (col == tablecol->compare_col) + return TRUE; + } + return FALSE; +} + +/* FIXME: This could be done in time log n instead of time n with a binary search. */ +gint +e_table_sorting_utils_insert (ETableModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + gint *map_table, + gint rows, + gint row) +{ + gint i; + gpointer cmp_cache = e_table_sorting_utils_create_cmp_cache (); + + i = 0; + /* handle insertions when we have a 'sort group' */ + while (i < rows && etsu_compare (source, sort_info, full_header, map_table[i], row, cmp_cache) < 0) + i++; + + e_table_sorting_utils_free_cmp_cache (cmp_cache); + + return i; +} + +/* FIXME: This could be done in time log n instead of time n with a binary search. */ +gint +e_table_sorting_utils_check_position (ETableModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + gint *map_table, + gint rows, + gint view_row) +{ + gint i; + gint row; + gpointer cmp_cache; + + i = view_row; + row = map_table[i]; + cmp_cache = e_table_sorting_utils_create_cmp_cache (); + + i = view_row; + if (i < rows - 1 && etsu_compare (source, sort_info, full_header, map_table[i + 1], row, cmp_cache) < 0) { + i++; + while (i < rows - 1 && etsu_compare (source, sort_info, full_header, map_table[i], row, cmp_cache) < 0) + i++; + } else if (i > 0 && etsu_compare (source, sort_info, full_header, map_table[i - 1], row, cmp_cache) > 0) { + i--; + while (i > 0 && etsu_compare (source, sort_info, full_header, map_table[i], row, cmp_cache) > 0) + i--; + } + + e_table_sorting_utils_free_cmp_cache (cmp_cache); + + return i; +} + +/* This takes source rows. */ +static gint +etsu_tree_compare (ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + ETreePath path1, + ETreePath path2, + gpointer cmp_cache) +{ + gint j; + gint sort_count = e_table_sort_info_sorting_get_count (sort_info); + gint comp_val = 0; + gint ascending = 1; + + for (j = 0; j < sort_count; j++) { + ETableSortColumn column = e_table_sort_info_sorting_get_nth (sort_info, j); + ETableCol *col; + col = e_table_header_get_column_by_col_idx (full_header, column.column); + if (col == NULL) + col = e_table_header_get_column (full_header, e_table_header_count (full_header) - 1); + comp_val = (*col->compare)(e_tree_model_value_at (source, path1, col->compare_col), + e_tree_model_value_at (source, path2, col->compare_col), + cmp_cache); + ascending = column.ascending; + if (comp_val != 0) + break; + } + if (!ascending) + comp_val = -comp_val; + return comp_val; +} + +static gint +e_sort_tree_callback (gconstpointer data1, + gconstpointer data2, + gpointer user_data) +{ + ETreePath *path1 = *(ETreePath *) data1; + ETreePath *path2 = *(ETreePath *) data2; + ETreeSortClosure *closure = user_data; + + return etsu_tree_compare (closure->tree, closure->sort_info, closure->full_header, path1, path2, closure->cmp_cache); +} + +void +e_table_sorting_utils_tree_sort (ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + ETreePath *map_table, + gint count) +{ + ETableSortClosure closure; + gint cols; + gint i, j; + gint *map; + ETreePath *map_copy; + g_return_if_fail (source != NULL); + g_return_if_fail (E_IS_TREE_MODEL (source)); + g_return_if_fail (sort_info != NULL); + g_return_if_fail (E_IS_TABLE_SORT_INFO (sort_info)); + g_return_if_fail (full_header != NULL); + g_return_if_fail (E_IS_TABLE_HEADER (full_header)); + + cols = e_table_sort_info_sorting_get_count (sort_info); + closure.cols = cols; + + closure.vals = g_new (gpointer , count * cols); + closure.ascending = g_new (int, cols); + closure.compare = g_new (GCompareDataFunc, cols); + closure.cmp_cache = e_table_sorting_utils_create_cmp_cache (); + + for (j = 0; j < cols; j++) { + ETableSortColumn column = e_table_sort_info_sorting_get_nth (sort_info, j); + ETableCol *col; + + col = e_table_header_get_column_by_col_idx (full_header, column.column); + if (col == NULL) + col = e_table_header_get_column (full_header, e_table_header_count (full_header) - 1); + + for (i = 0; i < count; i++) { + closure.vals[i * cols + j] = e_tree_model_sort_value_at (source, map_table[i], col->compare_col); + } + closure.ascending[j] = column.ascending; + closure.compare[j] = col->compare; + } + + map = g_new (int, count); + for (i = 0; i < count; i++) { + map[i] = i; + } + + g_qsort_with_data ( + map, count, sizeof (gint), e_sort_callback, &closure); + + map_copy = g_new (ETreePath, count); + for (i = 0; i < count; i++) { + map_copy[i] = map_table[i]; + } + for (i = 0; i < count; i++) { + map_table[i] = map_copy[map[i]]; + } + + g_free (map); + g_free (map_copy); + + g_free (closure.vals); + g_free (closure.ascending); + g_free (closure.compare); + e_table_sorting_utils_free_cmp_cache (closure.cmp_cache); +} + +/* FIXME: This could be done in time log n instead of time n with a binary search. */ +gint +e_table_sorting_utils_tree_check_position (ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + ETreePath *map_table, + gint count, + gint old_index) +{ + gint i; + ETreePath path; + gpointer cmp_cache = e_table_sorting_utils_create_cmp_cache (); + + i = old_index; + path = map_table[i]; + + if (i < count - 1 && etsu_tree_compare (source, sort_info, full_header, map_table[i + 1], path, cmp_cache) < 0) { + i++; + while (i < count - 1 && etsu_tree_compare (source, sort_info, full_header, map_table[i], path, cmp_cache) < 0) + i++; + } else if (i > 0 && etsu_tree_compare (source, sort_info, full_header, map_table[i - 1], path, cmp_cache) > 0) { + i--; + while (i > 0 && etsu_tree_compare (source, sort_info, full_header, map_table[i], path, cmp_cache) > 0) + i--; + } + + e_table_sorting_utils_free_cmp_cache (cmp_cache); + + return i; +} + +/* FIXME: This does not pay attention to making sure that it's a stable insert. This needs to be fixed. */ +gint +e_table_sorting_utils_tree_insert (ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + ETreePath *map_table, + gint count, + ETreePath path) +{ + gsize start; + gsize end; + ETreeSortClosure closure; + + closure.tree = source; + closure.sort_info = sort_info; + closure.full_header = full_header; + closure.cmp_cache = e_table_sorting_utils_create_cmp_cache (); + + e_bsearch (&path, map_table, count, sizeof (ETreePath), e_sort_tree_callback, &closure, &start, &end); + + e_table_sorting_utils_free_cmp_cache (closure.cmp_cache); + + return end; +} + +/** + * e_table_sorting_utils_create_cmp_cache: + * + * Creates a new compare cache, which is storing pairs of string keys and + * string values. This can be accessed by + * e_table_sorting_utils_lookup_cmp_cache() and + * e_table_sorting_utils_add_to_cmp_cache(). + * + * Returned pointer should be freed with + * e_table_sorting_utils_free_cmp_cache(). + **/ +gpointer +e_table_sorting_utils_create_cmp_cache (void) +{ + return g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify) camel_pstring_free, g_free); +} + +/** + * e_table_sorting_utils_free_cmp_cache: + * @cmp_cache: a compare cache; cannot be %NULL + * + * Frees a compare cache previously created with + * e_table_sorting_utils_create_cmp_cache(). + **/ +void +e_table_sorting_utils_free_cmp_cache (gpointer cmp_cache) +{ + g_return_if_fail (cmp_cache != NULL); + + g_hash_table_destroy (cmp_cache); +} + +/** + * e_table_sorting_utils_add_to_cmp_cache: + * @cmp_cache: a compare cache; cannot be %NULL + * @key: unique key to a cache; cannot be %NULL + * @value: value to store for a key + * + * Adds a new value for a given key to a compare cache. If such key + * already exists in a cache then its value will be replaced. + * Note: Given @value will be stolen and later freed with g_free. + **/ +void +e_table_sorting_utils_add_to_cmp_cache (gpointer cmp_cache, + const gchar *key, + gchar *value) +{ + g_return_if_fail (cmp_cache != NULL); + g_return_if_fail (key != NULL); + + g_hash_table_insert (cmp_cache, (gchar *) camel_pstring_strdup (key), value); +} + +/** + * e_table_sorting_utils_lookup_cmp_cache: + * @cmp_cache: a compare cache + * @key: unique key to a cache + * + * Lookups for a key in a compare cache, which is passed in GCompareDataFunc as 'data'. + * Returns %NULL when not found or the cache wasn't provided, otherwise value stored + * with a key. + **/ +const gchar * +e_table_sorting_utils_lookup_cmp_cache (gpointer cmp_cache, + const gchar *key) +{ + g_return_val_if_fail (key != NULL, NULL); + + if (!cmp_cache) + return NULL; + + return g_hash_table_lookup (cmp_cache, key); +} diff --git a/e-util/e-table-sorting-utils.h b/e-util/e-table-sorting-utils.h new file mode 100644 index 0000000000..2d5ccb4363 --- /dev/null +++ b/e-util/e-table-sorting-utils.h @@ -0,0 +1,95 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SORTING_UTILS_H_ +#define _E_TABLE_SORTING_UTILS_H_ + +#include <e-util/e-table-header.h> +#include <e-util/e-table-model.h> +#include <e-util/e-table-sort-info.h> +#include <e-util/e-tree-model.h> + +G_BEGIN_DECLS + +gboolean e_table_sorting_utils_affects_sort + (ETableSortInfo *sort_info, + ETableHeader *full_header, + gint col); + +void e_table_sorting_utils_sort (ETableModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + gint *map_table, + gint rows); +gint e_table_sorting_utils_insert (ETableModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + gint *map_table, + gint rows, + gint row); +gint e_table_sorting_utils_check_position + (ETableModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + gint *map_table, + gint rows, + gint view_row); + +void e_table_sorting_utils_tree_sort (ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + ETreePath *map_table, + gint count); +gint e_table_sorting_utils_tree_check_position + (ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + ETreePath *map_table, + gint count, + gint old_index); +gint e_table_sorting_utils_tree_insert + (ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *full_header, + ETreePath *map_table, + gint count, + ETreePath path); + +gpointer e_table_sorting_utils_create_cmp_cache + (void); +void e_table_sorting_utils_free_cmp_cache + (gpointer cmp_cache); +void e_table_sorting_utils_add_to_cmp_cache + (gpointer cmp_cache, + const gchar *key, + gchar *value); +const gchar * e_table_sorting_utils_lookup_cmp_cache + (gpointer cmp_cache, + const gchar *key); + +G_END_DECLS + +#endif /* _E_TABLE_SORTING_UTILS_H_ */ diff --git a/e-util/e-table-specification.c b/e-util/e-table-specification.c new file mode 100644 index 0000000000..03cb429131 --- /dev/null +++ b/e-util/e-table-specification.c @@ -0,0 +1,435 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-specification.h" + +#include <stdlib.h> +#include <string.h> + +#include <glib/gstdio.h> +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include <libedataserver/libedataserver.h> + +#include "e-xml-utils.h" + +/* workaround for avoiding API breakage */ +#define etsp_get_type e_table_specification_get_type +G_DEFINE_TYPE (ETableSpecification, etsp, G_TYPE_OBJECT) + +static void +etsp_finalize (GObject *object) +{ + ETableSpecification *etsp = E_TABLE_SPECIFICATION (object); + gint i; + + if (etsp->columns) { + for (i = 0; etsp->columns[i]; i++) { + g_object_unref (etsp->columns[i]); + } + g_free (etsp->columns); + etsp->columns = NULL; + } + + if (etsp->state) + g_object_unref (etsp->state); + etsp->state = NULL; + + g_free (etsp->click_to_add_message); + etsp->click_to_add_message = NULL; + + g_free (etsp->domain); + etsp->domain = NULL; + + G_OBJECT_CLASS (etsp_parent_class)->finalize (object); +} + +static void +etsp_class_init (ETableSpecificationClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = etsp_finalize; +} + +static void +etsp_init (ETableSpecification *etsp) +{ + etsp->columns = NULL; + etsp->state = NULL; + + etsp->alternating_row_colors = TRUE; + etsp->no_headers = FALSE; + etsp->click_to_add = FALSE; + etsp->click_to_add_end = FALSE; + etsp->horizontal_draw_grid = FALSE; + etsp->vertical_draw_grid = FALSE; + etsp->draw_focus = TRUE; + etsp->horizontal_scrolling = FALSE; + etsp->horizontal_resize = FALSE; + etsp->allow_grouping = TRUE; + + etsp->cursor_mode = E_CURSOR_SIMPLE; + etsp->selection_mode = GTK_SELECTION_MULTIPLE; + + etsp->click_to_add_message = NULL; + etsp->domain = NULL; +} + +/** + * e_table_specification_new: + * + * Creates a new %ETableSpecification object. This object is used to hold the + * information about the rendering information for ETable. + * + * Returns: a newly created %ETableSpecification object. + */ +ETableSpecification * +e_table_specification_new (void) +{ + ETableSpecification *etsp = g_object_new (E_TYPE_TABLE_SPECIFICATION, NULL); + + return (ETableSpecification *) etsp; +} + +/** + * e_table_specification_load_from_file: + * @specification: An ETableSpecification that you want to modify + * @filename: a filename that contains an ETableSpecification + * + * This routine modifies @specification to reflect the state described + * by the file @filename. + * + * Returns: TRUE on success, FALSE on failure. + */ +gboolean +e_table_specification_load_from_file (ETableSpecification *specification, + const gchar *filename) +{ + xmlDoc *doc; + + doc = e_xml_parse_file (filename); + if (doc) { + xmlNode *node = xmlDocGetRootElement (doc); + e_table_specification_load_from_node (specification, node); + xmlFreeDoc (doc); + return TRUE; + } + return FALSE; +} + +/** + * e_table_specification_load_from_string: + * @specification: An ETableSpecification that you want to modify + * @xml: a stringified representation of an ETableSpecification description. + * + * This routine modifies @specification to reflect the state described + * by @xml. @xml is typically returned by e_table_specification_save_to_string + * or it can be embedded in your source code. + * + * Returns: TRUE on success, FALSE on failure. + */ +gboolean +e_table_specification_load_from_string (ETableSpecification *specification, + const gchar *xml) +{ + xmlDoc *doc; + doc = xmlParseMemory ((gchar *) xml, strlen (xml)); + if (doc) { + xmlNode *node = xmlDocGetRootElement (doc); + e_table_specification_load_from_node (specification, node); + xmlFreeDoc (doc); + return TRUE; + } + + return FALSE; +} + +/** + * e_table_specification_load_from_node: + * @specification: An ETableSpecification that you want to modify + * @node: an xmlNode with an XML ETableSpecification description. + * + * This routine modifies @specification to reflect the state described + * by @node. + */ +void +e_table_specification_load_from_node (ETableSpecification *specification, + const xmlNode *node) +{ + gchar *temp; + xmlNode *children; + GList *list = NULL, *list2; + gint i; + + specification->no_headers = e_xml_get_bool_prop_by_name (node, (const guchar *)"no-headers"); + specification->click_to_add = e_xml_get_bool_prop_by_name (node, (const guchar *)"click-to-add"); + specification->click_to_add_end = e_xml_get_bool_prop_by_name (node, (const guchar *)"click-to-add-end") && specification->click_to_add; + specification->alternating_row_colors = e_xml_get_bool_prop_by_name_with_default (node, (const guchar *)"alternating-row-colors", TRUE); + specification->horizontal_draw_grid = e_xml_get_bool_prop_by_name (node, (const guchar *)"horizontal-draw-grid"); + specification->vertical_draw_grid = e_xml_get_bool_prop_by_name (node, (const guchar *)"vertical-draw-grid"); + if (e_xml_get_bool_prop_by_name_with_default (node, (const guchar *)"draw-grid", TRUE) == + e_xml_get_bool_prop_by_name_with_default (node, (const guchar *)"draw-grid", FALSE)) { + specification->horizontal_draw_grid = + specification->vertical_draw_grid = e_xml_get_bool_prop_by_name (node, (const guchar *)"draw-grid"); + } + specification->draw_focus = e_xml_get_bool_prop_by_name_with_default (node, (const guchar *)"draw-focus", TRUE); + specification->horizontal_scrolling = e_xml_get_bool_prop_by_name_with_default (node, (const guchar *)"horizontal-scrolling", FALSE); + specification->horizontal_resize = e_xml_get_bool_prop_by_name_with_default (node, (const guchar *)"horizontal-resize", FALSE); + specification->allow_grouping = e_xml_get_bool_prop_by_name_with_default (node, (const guchar *)"allow-grouping", TRUE); + + specification->selection_mode = GTK_SELECTION_MULTIPLE; + temp = e_xml_get_string_prop_by_name (node, (const guchar *)"selection-mode"); + if (temp && !g_ascii_strcasecmp (temp, "single")) { + specification->selection_mode = GTK_SELECTION_SINGLE; + } else if (temp && !g_ascii_strcasecmp (temp, "browse")) { + specification->selection_mode = GTK_SELECTION_BROWSE; + } else if (temp && !g_ascii_strcasecmp (temp, "extended")) { + specification->selection_mode = GTK_SELECTION_MULTIPLE; + } + g_free (temp); + + specification->cursor_mode = E_CURSOR_SIMPLE; + temp = e_xml_get_string_prop_by_name (node, (const guchar *)"cursor-mode"); + if (temp && !g_ascii_strcasecmp (temp, "line")) { + specification->cursor_mode = E_CURSOR_LINE; + } else if (temp && !g_ascii_strcasecmp (temp, "spreadsheet")) { + specification->cursor_mode = E_CURSOR_SPREADSHEET; + } + g_free (temp); + + g_free (specification->click_to_add_message); + specification->click_to_add_message = + e_xml_get_string_prop_by_name ( + node, (const guchar *)"_click-to-add-message"); + + g_free (specification->domain); + specification->domain = + e_xml_get_string_prop_by_name ( + node, (const guchar *)"gettext-domain"); + if (specification->domain && !*specification->domain) { + g_free (specification->domain); + specification->domain = NULL; + } + + if (specification->state) + g_object_unref (specification->state); + specification->state = NULL; + if (specification->columns) { + for (i = 0; specification->columns[i]; i++) { + g_object_unref (specification->columns[i]); + } + g_free (specification->columns); + } + specification->columns = NULL; + + for (children = node->xmlChildrenNode; children; children = children->next) { + if (!strcmp ((gchar *) children->name, "ETableColumn")) { + ETableColumnSpecification *col_spec = e_table_column_specification_new (); + + e_table_column_specification_load_from_node (col_spec, children); + list = g_list_append (list, col_spec); + } else if (specification->state == NULL && !strcmp ((gchar *) children->name, "ETableState")) { + specification->state = e_table_state_new (); + e_table_state_load_from_node (specification->state, children); + e_table_sort_info_set_can_group (specification->state->sort_info, specification->allow_grouping); + } + } + + if (specification->state == NULL) { + /* Make the default state. */ + specification->state = e_table_state_vanilla (g_list_length (list)); + } + + specification->columns = g_new (ETableColumnSpecification *, g_list_length (list) + 1); + for (list2 = list, i = 0; list2; list2 = g_list_next (list2), i++) { + specification->columns[i] = list2->data; + } + specification->columns[i] = NULL; + g_list_free (list); +} + +/** + * e_table_specification_save_to_file: + * @specification: An %ETableSpecification that you want to save + * @filename: a file name to store the specification. + * + * This routine stores the @specification into @filename. + * + * Returns: 0 on success or -1 on error. + */ +gint +e_table_specification_save_to_file (ETableSpecification *specification, + const gchar *filename) +{ + xmlDoc *doc; + gint ret; + + g_return_val_if_fail (specification != NULL, -1); + g_return_val_if_fail (filename != NULL, -1); + g_return_val_if_fail (E_IS_TABLE_SPECIFICATION (specification), -1); + + if ((doc = xmlNewDoc ((const guchar *)"1.0")) == NULL) + return -1; + + xmlDocSetRootElement (doc, e_table_specification_save_to_node (specification, doc)); + + ret = e_xml_save_file (filename, doc); + + xmlFreeDoc (doc); + + return ret; +} + +/** + * e_table_specification_save_to_string: + * @specification: An %ETableSpecification that you want to stringify + * + * Saves the state of @specification to a string. + * + * Returns: an g_alloc() allocated string containing the stringified + * representation of @specification. This stringified representation + * uses XML as a convenience. + */ +gchar * +e_table_specification_save_to_string (ETableSpecification *specification) +{ + gchar *ret_val; + xmlChar *string; + gint length; + xmlDoc *doc; + + g_return_val_if_fail (specification != NULL, NULL); + g_return_val_if_fail (E_IS_TABLE_SPECIFICATION (specification), NULL); + + doc = xmlNewDoc ((const guchar *)"1.0"); + xmlDocSetRootElement (doc, e_table_specification_save_to_node (specification, doc)); + xmlDocDumpMemory (doc, &string, &length); + + ret_val = g_strdup ((gchar *) string); + xmlFree (string); + return ret_val; +} + +/** + * e_table_specification_save_to_node: + * @specification: An ETableSpecification that you want to store. + * @doc: Node where the specification is saved + * + * This routine saves the %ETableSpecification state in the object @specification + * into the xmlDoc represented by @doc. + * + * Returns: The node that has been attached to @doc with the contents + * of the ETableSpecification. + */ +xmlNode * +e_table_specification_save_to_node (ETableSpecification *specification, + xmlDoc *doc) +{ + xmlNode *node; + const gchar *s; + + g_return_val_if_fail (doc != NULL, NULL); + g_return_val_if_fail (specification != NULL, NULL); + g_return_val_if_fail (E_IS_TABLE_SPECIFICATION (specification), NULL); + + node = xmlNewNode (NULL, (const guchar *)"ETableSpecification"); + e_xml_set_bool_prop_by_name (node, (const guchar *)"no-headers", specification->no_headers); + e_xml_set_bool_prop_by_name (node, (const guchar *)"click-to-add", specification->click_to_add); + e_xml_set_bool_prop_by_name (node, (const guchar *)"click-to-add-end", specification->click_to_add_end && specification->click_to_add); + e_xml_set_bool_prop_by_name (node, (const guchar *)"alternating-row-colors", specification->alternating_row_colors); + e_xml_set_bool_prop_by_name (node, (const guchar *)"horizontal-draw-grid", specification->horizontal_draw_grid); + e_xml_set_bool_prop_by_name (node, (const guchar *)"vertical-draw-grid", specification->vertical_draw_grid); + e_xml_set_bool_prop_by_name (node, (const guchar *)"draw-focus", specification->draw_focus); + e_xml_set_bool_prop_by_name (node, (const guchar *)"horizontal-scrolling", specification->horizontal_scrolling); + e_xml_set_bool_prop_by_name (node, (const guchar *)"horizontal-resize", specification->horizontal_resize); + e_xml_set_bool_prop_by_name (node, (const guchar *)"allow-grouping", specification->allow_grouping); + + switch (specification->selection_mode) { + case GTK_SELECTION_SINGLE: + s = "single"; + break; + case GTK_SELECTION_BROWSE: + s = "browse"; + break; + default: + case GTK_SELECTION_MULTIPLE: + s = "extended"; + } + xmlSetProp (node, (const guchar *)"selection-mode", (guchar *) s); + if (specification->cursor_mode == E_CURSOR_LINE) + s = "line"; + else + s = "cell"; + xmlSetProp (node, (const guchar *)"cursor-mode", (guchar *) s); + + xmlSetProp (node, (const guchar *)"_click-to-add-message", (guchar *) specification->click_to_add_message); + xmlSetProp (node, (const guchar *)"gettext-domain", (guchar *) specification->domain); + + if (specification->columns) { + gint i; + + for (i = 0; specification->columns[i]; i++) + e_table_column_specification_save_to_node ( + specification->columns[i], + node); + } + + if (specification->state) + e_table_state_save_to_node (specification->state, node); + + return node; +} + +/** + * e_table_specification_duplicate: + * @spec: specification to duplicate + * + * This creates a copy of the %ETableSpecification @spec + * + * Returns: The duplicated %ETableSpecification. + */ +ETableSpecification * +e_table_specification_duplicate (ETableSpecification *spec) +{ + ETableSpecification *new_spec; + gchar *spec_str; + + g_return_val_if_fail (spec != NULL, NULL); + g_return_val_if_fail (E_IS_TABLE_SPECIFICATION (spec), NULL); + + new_spec = e_table_specification_new (); + spec_str = e_table_specification_save_to_string (spec); + if (!e_table_specification_load_from_string (new_spec, spec_str)) { + g_warning ("Unable to duplicate ETable specification"); + g_object_unref (new_spec); + new_spec = NULL; + } + g_free (spec_str); + + return new_spec; +} diff --git a/e-util/e-table-specification.h b/e-util/e-table-specification.h new file mode 100644 index 0000000000..8ed43aed73 --- /dev/null +++ b/e-util/e-table-specification.h @@ -0,0 +1,116 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SPECIFICATION_H_ +#define _E_TABLE_SPECIFICATION_H_ + +#include <libxml/tree.h> + +#include <e-util/e-selection-model.h> +#include <e-util/e-table-column-specification.h> +#include <e-util/e-table-defines.h> +#include <e-util/e-table-state.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_SPECIFICATION \ + (e_table_specification_get_type ()) +#define E_TABLE_SPECIFICATION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_SPECIFICATION, ETableSpecification)) +#define E_TABLE_SPECIFICATION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_SPECIFICATION, ETableSpecificationClass)) +#define E_IS_TABLE_SPECIFICATION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_SPECIFICATION)) +#define E_IS_TABLE_SPECIFICATION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_SPECIFICATION)) +#define E_TABLE_SPECIFICATION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_SPECIFICATION, ETableSpecificationClass)) + +G_BEGIN_DECLS + +typedef struct _ETableSpecification ETableSpecification; +typedef struct _ETableSpecificationClass ETableSpecificationClass; + +struct _ETableSpecification { + GObject parent; + + ETableColumnSpecification **columns; + ETableState *state; + + guint alternating_row_colors : 1; + guint no_headers : 1; + guint click_to_add : 1; + guint click_to_add_end : 1; + guint horizontal_draw_grid : 1; + guint vertical_draw_grid : 1; + guint draw_focus : 1; + guint horizontal_scrolling : 1; + guint horizontal_resize : 1; + guint allow_grouping : 1; + GtkSelectionMode selection_mode; + ECursorMode cursor_mode; + + gchar *click_to_add_message; + gchar *domain; +}; + +struct _ETableSpecificationClass { + GObjectClass parent_class; +}; + +GType e_table_specification_get_type (void) G_GNUC_CONST; +ETableSpecification * + e_table_specification_new (void); + +gboolean e_table_specification_load_from_file + (ETableSpecification *specification, + const gchar *filename); +gboolean e_table_specification_load_from_string + (ETableSpecification *specification, + const gchar *xml); +void e_table_specification_load_from_node + (ETableSpecification *specification, + const xmlNode *node); + +gint e_table_specification_save_to_file + (ETableSpecification *specification, + const gchar *filename); +gchar * e_table_specification_save_to_string + (ETableSpecification *specification); +xmlNode * e_table_specification_save_to_node + (ETableSpecification *specification, + xmlDoc *doc); +ETableSpecification * + e_table_specification_duplicate (ETableSpecification *specification); + +G_END_DECLS + +#endif /* _E_TABLE_SPECIFICATION_H_ */ diff --git a/e-util/e-table-state.c b/e-util/e-table-state.c new file mode 100644 index 0000000000..e5253be7c9 --- /dev/null +++ b/e-util/e-table-state.c @@ -0,0 +1,320 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-state.h" + +#include <stdlib.h> +#include <string.h> + +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include <libedataserver/libedataserver.h> + +#include "e-xml-utils.h" + +#define STATE_VERSION 0.1 + +G_DEFINE_TYPE (ETableState, e_table_state, G_TYPE_OBJECT) + +static void +etst_dispose (GObject *object) +{ + ETableState *etst = E_TABLE_STATE (object); + + if (etst->sort_info) { + g_object_unref (etst->sort_info); + etst->sort_info = NULL; + } + + G_OBJECT_CLASS (e_table_state_parent_class)->dispose (object); +} + +static void +etst_finalize (GObject *object) +{ + ETableState *etst = E_TABLE_STATE (object); + + if (etst->columns) { + g_free (etst->columns); + etst->columns = NULL; + } + + if (etst->expansions) { + g_free (etst->expansions); + etst->expansions = NULL; + } + + G_OBJECT_CLASS (e_table_state_parent_class)->finalize (object); +} + +static void +e_table_state_class_init (ETableStateClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = etst_dispose; + object_class->finalize = etst_finalize; +} + +static void +e_table_state_init (ETableState *state) +{ + state->columns = NULL; + state->expansions = NULL; + state->sort_info = e_table_sort_info_new (); +} + +ETableState * +e_table_state_new (void) +{ + return g_object_new (E_TYPE_TABLE_STATE, NULL); +} + +ETableState * +e_table_state_vanilla (gint col_count) +{ + GString *str; + gint i; + ETableState *res; + + str = g_string_new ("<ETableState>\n"); + for (i = 0; i < col_count; i++) + g_string_append_printf (str, " <column source=\"%d\"/>\n", i); + g_string_append (str, " <grouping></grouping>\n"); + g_string_append (str, "</ETableState>\n"); + + res = e_table_state_new (); + e_table_state_load_from_string (res, str->str); + + g_string_free (str, TRUE); + return res; +} + +gboolean +e_table_state_load_from_file (ETableState *state, + const gchar *filename) +{ + xmlDoc *doc; + + g_return_val_if_fail (E_IS_TABLE_STATE (state), FALSE); + g_return_val_if_fail (filename != NULL, FALSE); + + doc = e_xml_parse_file (filename); + if (doc) { + xmlNode *node = xmlDocGetRootElement (doc); + e_table_state_load_from_node (state, node); + xmlFreeDoc (doc); + return TRUE; + } + return FALSE; +} + +void +e_table_state_load_from_string (ETableState *state, + const gchar *xml) +{ + xmlDoc *doc; + + g_return_if_fail (E_IS_TABLE_STATE (state)); + g_return_if_fail (xml != NULL); + + doc = xmlParseMemory ((gchar *) xml, strlen (xml)); + if (doc) { + xmlNode *node = xmlDocGetRootElement (doc); + e_table_state_load_from_node (state, node); + xmlFreeDoc (doc); + } +} + +typedef struct { + gint column; + gdouble expansion; +} int_and_double; + +void +e_table_state_load_from_node (ETableState *state, + const xmlNode *node) +{ + xmlNode *children; + GList *list = NULL, *iterator; + gdouble state_version; + gint i; + gboolean can_group = TRUE; + + g_return_if_fail (E_IS_TABLE_STATE (state)); + g_return_if_fail (node != NULL); + + state_version = e_xml_get_double_prop_by_name_with_default ( + node, (const guchar *)"state-version", STATE_VERSION); + + if (state->sort_info) { + can_group = e_table_sort_info_get_can_group (state->sort_info); + g_object_unref (state->sort_info); + } + + state->sort_info = NULL; + children = node->xmlChildrenNode; + for (; children; children = children->next) { + if (!strcmp ((gchar *) children->name, "column")) { + int_and_double *column_info = g_new (int_and_double, 1); + + column_info->column = e_xml_get_integer_prop_by_name ( + children, (const guchar *)"source"); + column_info->expansion = + e_xml_get_double_prop_by_name_with_default ( + children, (const guchar *)"expansion", 1); + + list = g_list_append (list, column_info); + } else if (state->sort_info == NULL && + !strcmp ((gchar *) children->name, "grouping")) { + state->sort_info = e_table_sort_info_new (); + e_table_sort_info_load_from_node ( + state->sort_info, children, state_version); + } + } + g_free (state->columns); + g_free (state->expansions); + state->col_count = g_list_length (list); + state->columns = g_new (int, state->col_count); + state->expansions = g_new (double, state->col_count); + + if (!state->sort_info) + state->sort_info = e_table_sort_info_new (); + e_table_sort_info_set_can_group (state->sort_info, can_group); + + for (iterator = list, i = 0; iterator; i++) { + int_and_double *column_info = iterator->data; + + state->columns[i] = column_info->column; + state->expansions[i] = column_info->expansion; + g_free (column_info); + iterator = g_list_next (iterator); + } + g_list_free (list); +} + +void +e_table_state_save_to_file (ETableState *state, + const gchar *filename) +{ + xmlDoc *doc; + + if ((doc = xmlNewDoc ((const guchar *)"1.0")) == NULL) + return; + + xmlDocSetRootElement (doc, e_table_state_save_to_node (state, NULL)); + + e_xml_save_file (filename, doc); + + xmlFreeDoc (doc); +} + +gchar * +e_table_state_save_to_string (ETableState *state) +{ + gchar *ret_val; + xmlChar *string; + gint length; + xmlDoc *doc; + + g_return_val_if_fail (E_IS_TABLE_STATE (state), NULL); + + doc = xmlNewDoc ((const guchar *)"1.0"); + xmlDocSetRootElement (doc, e_table_state_save_to_node (state, NULL)); + xmlDocDumpMemory (doc, &string, &length); + xmlFreeDoc (doc); + + ret_val = g_strdup ((gchar *) string); + xmlFree (string); + return ret_val; +} + +xmlNode * +e_table_state_save_to_node (ETableState *state, + xmlNode *parent) +{ + gint i; + xmlNode *node; + + g_return_val_if_fail (E_IS_TABLE_STATE (state), NULL); + + if (parent) + node = xmlNewChild ( + parent, NULL, (const guchar *) "ETableState", NULL); + else + node = xmlNewNode (NULL, (const guchar *) "ETableState"); + + e_xml_set_double_prop_by_name ( + node, (const guchar *)"state-version", STATE_VERSION); + + for (i = 0; i < state->col_count; i++) { + gint column = state->columns[i]; + gdouble expansion = state->expansions[i]; + xmlNode *new_node; + + new_node = xmlNewChild ( + node, NULL, (const guchar *) "column", NULL); + e_xml_set_integer_prop_by_name ( + new_node, (const guchar *) "source", column); + if (expansion >= -1) + e_xml_set_double_prop_by_name ( + new_node, (const guchar *) + "expansion", expansion); + } + + e_table_sort_info_save_to_node (state->sort_info, node); + + return node; +} + +/** + * e_table_state_duplicate: + * @state: The ETableState to duplicate + * + * This creates a copy of the %ETableState @state + * + * Returns: The duplicated %ETableState. + */ +ETableState * +e_table_state_duplicate (ETableState *state) +{ + ETableState *new_state; + gchar *copy; + + g_return_val_if_fail (E_IS_TABLE_STATE (state), NULL); + + new_state = e_table_state_new (); + copy = e_table_state_save_to_string (state); + e_table_state_load_from_string (new_state, copy); + g_free (copy); + + e_table_sort_info_set_can_group + (new_state->sort_info, + e_table_sort_info_get_can_group (state->sort_info)); + + return new_state; +} diff --git a/e-util/e-table-state.h b/e-util/e-table-state.h new file mode 100644 index 0000000000..ac3cfc2879 --- /dev/null +++ b/e-util/e-table-state.h @@ -0,0 +1,89 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_STATE_H_ +#define _E_TABLE_STATE_H_ + +#include <libxml/tree.h> + +#include <e-util/e-table-sort-info.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_STATE \ + (e_table_state_get_type ()) +#define E_TABLE_STATE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_STATE, ETableState)) +#define E_TABLE_STATE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_STATE, ETableStateClass)) +#define E_IS_TABLE_STATE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_STATE)) +#define E_IS_TABLE_STATE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_STATE)) +#define E_TABLE_STATE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_STATE, ETableStateClass)) + +G_BEGIN_DECLS + +typedef struct _ETableState ETableState; +typedef struct _ETableStateClass ETableStateClass; + +struct _ETableState { + GObject parent; + + ETableSortInfo *sort_info; + gint col_count; + gint *columns; + gdouble *expansions; +}; + +struct _ETableStateClass { + GObjectClass parent_class; +}; + +GType e_table_state_get_type (void) G_GNUC_CONST; +ETableState * e_table_state_new (void); +ETableState * e_table_state_vanilla (gint col_count); +gboolean e_table_state_load_from_file (ETableState *state, + const gchar *filename); +void e_table_state_load_from_string (ETableState *state, + const gchar *xml); +void e_table_state_load_from_node (ETableState *state, + const xmlNode *node); +void e_table_state_save_to_file (ETableState *state, + const gchar *filename); +gchar * e_table_state_save_to_string (ETableState *state); +xmlNode * e_table_state_save_to_node (ETableState *state, + xmlNode *parent); +ETableState * e_table_state_duplicate (ETableState *state); + +G_END_DECLS + +#endif /* _E_TABLE_STATE_H_ */ diff --git a/e-util/e-table-subset-variable.c b/e-util/e-table-subset-variable.c new file mode 100644 index 0000000000..8d9f3d0c8d --- /dev/null +++ b/e-util/e-table-subset-variable.c @@ -0,0 +1,267 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <string.h> + +#include "e-table-subset-variable.h" + +#define ETSSV_CLASS(e) (E_TABLE_SUBSET_VARIABLE_GET_CLASS (e)) + +/* workaround for avoiding API breakage */ +#define etssv_get_type e_table_subset_variable_get_type +G_DEFINE_TYPE (ETableSubsetVariable, etssv, E_TYPE_TABLE_SUBSET) + +#define INCREMENT_AMOUNT 10 + +static void +etssv_add (ETableSubsetVariable *etssv, + gint row) +{ + ETableModel *etm = E_TABLE_MODEL (etssv); + ETableSubset *etss = E_TABLE_SUBSET (etssv); + + e_table_model_pre_change (etm); + + if (etss->n_map + 1 > etssv->n_vals_allocated) { + etssv->n_vals_allocated += INCREMENT_AMOUNT; + etss->map_table = g_realloc ( + etss->map_table, + etssv->n_vals_allocated * sizeof (gint)); + } + + etss->map_table[etss->n_map++] = row; + + e_table_model_row_inserted (etm, etss->n_map - 1); +} + +static void +etssv_add_array (ETableSubsetVariable *etssv, + const gint *array, + gint count) +{ + ETableModel *etm = E_TABLE_MODEL (etssv); + ETableSubset *etss = E_TABLE_SUBSET (etssv); + gint i; + + e_table_model_pre_change (etm); + + if (etss->n_map + count > etssv->n_vals_allocated) { + etssv->n_vals_allocated += MAX (INCREMENT_AMOUNT, count); + etss->map_table = g_realloc ( + etss->map_table, + etssv->n_vals_allocated * sizeof (gint)); + } + for (i = 0; i < count; i++) + etss->map_table[etss->n_map++] = array[i]; + + e_table_model_changed (etm); +} + +static void +etssv_add_all (ETableSubsetVariable *etssv) +{ + ETableModel *etm = E_TABLE_MODEL (etssv); + ETableSubset *etss = E_TABLE_SUBSET (etssv); + gint rows; + gint i; + + e_table_model_pre_change (etm); + + rows = e_table_model_row_count (etss->source); + if (etss->n_map + rows > etssv->n_vals_allocated) { + etssv->n_vals_allocated += MAX (INCREMENT_AMOUNT, rows); + etss->map_table = g_realloc ( + etss->map_table, + etssv->n_vals_allocated * sizeof (gint)); + } + for (i = 0; i < rows; i++) + etss->map_table[etss->n_map++] = i; + + e_table_model_changed (etm); +} + +static gboolean +etssv_remove (ETableSubsetVariable *etssv, + gint row) +{ + ETableModel *etm = E_TABLE_MODEL (etssv); + ETableSubset *etss = E_TABLE_SUBSET (etssv); + gint i; + + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] == row) { + e_table_model_pre_change (etm); + memmove ( + etss->map_table + i, + etss->map_table + i + 1, + (etss->n_map - i - 1) * sizeof (gint)); + etss->n_map--; + + e_table_model_row_deleted (etm, i); + return TRUE; + } + } + return FALSE; +} + +static void +etssv_class_init (ETableSubsetVariableClass *class) +{ + class->add = etssv_add; + class->add_array = etssv_add_array; + class->add_all = etssv_add_all; + class->remove = etssv_remove; +} + +static void +etssv_init (ETableSubsetVariable *etssv) +{ + /* nothing to do */ +} + +ETableModel * +e_table_subset_variable_construct (ETableSubsetVariable *etssv, + ETableModel *source) +{ + if (e_table_subset_construct (E_TABLE_SUBSET (etssv), source, 1) == NULL) + return NULL; + E_TABLE_SUBSET (etssv)->n_map = 0; + + return E_TABLE_MODEL (etssv); +} + +ETableModel * +e_table_subset_variable_new (ETableModel *source) +{ + ETableSubsetVariable *etssv = g_object_new (E_TYPE_TABLE_SUBSET_VARIABLE, NULL); + + if (e_table_subset_variable_construct (etssv, source) == NULL) { + g_object_unref (etssv); + return NULL; + } + + return (ETableModel *) etssv; +} + +void +e_table_subset_variable_add (ETableSubsetVariable *etssv, + gint row) +{ + g_return_if_fail (etssv != NULL); + g_return_if_fail (E_IS_TABLE_SUBSET_VARIABLE (etssv)); + + if (ETSSV_CLASS (etssv)->add) + ETSSV_CLASS (etssv)->add (etssv, row); +} + +void +e_table_subset_variable_add_array (ETableSubsetVariable *etssv, + const gint *array, + gint count) +{ + g_return_if_fail (etssv != NULL); + g_return_if_fail (E_IS_TABLE_SUBSET_VARIABLE (etssv)); + + if (ETSSV_CLASS (etssv)->add_array) + ETSSV_CLASS (etssv)->add_array (etssv, array, count); +} + +void +e_table_subset_variable_add_all (ETableSubsetVariable *etssv) +{ + g_return_if_fail (etssv != NULL); + g_return_if_fail (E_IS_TABLE_SUBSET_VARIABLE (etssv)); + + if (ETSSV_CLASS (etssv)->add_all) + ETSSV_CLASS (etssv)->add_all (etssv); +} + +gboolean +e_table_subset_variable_remove (ETableSubsetVariable *etssv, + gint row) +{ + g_return_val_if_fail (etssv != NULL, FALSE); + g_return_val_if_fail (E_IS_TABLE_SUBSET_VARIABLE (etssv), FALSE); + + if (ETSSV_CLASS (etssv)->remove) + return ETSSV_CLASS (etssv)->remove (etssv, row); + else + return FALSE; +} + +void +e_table_subset_variable_clear (ETableSubsetVariable *etssv) +{ + ETableModel *etm = E_TABLE_MODEL (etssv); + ETableSubset *etss = E_TABLE_SUBSET (etssv); + + e_table_model_pre_change (etm); + etss->n_map = 0; + g_free (etss->map_table); + etss->map_table = (gint *) g_new (guint, 1); + etssv->n_vals_allocated = 1; + + e_table_model_changed (etm); +} + +void +e_table_subset_variable_increment (ETableSubsetVariable *etssv, + gint position, + gint amount) +{ + gint i; + ETableSubset *etss = E_TABLE_SUBSET (etssv); + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] >= position) + etss->map_table[i] += amount; + } +} + +void +e_table_subset_variable_decrement (ETableSubsetVariable *etssv, + gint position, + gint amount) +{ + gint i; + ETableSubset *etss = E_TABLE_SUBSET (etssv); + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] >= position) + etss->map_table[i] -= amount; + } +} + +void +e_table_subset_variable_set_allocation (ETableSubsetVariable *etssv, + gint total) +{ + ETableSubset *etss = E_TABLE_SUBSET (etssv); + if (total <= 0) + total = 1; + if (total > etss->n_map) { + etss->map_table = g_realloc (etss->map_table, total * sizeof (gint)); + } +} diff --git a/e-util/e-table-subset-variable.h b/e-util/e-table-subset-variable.h new file mode 100644 index 0000000000..ca4adddd18 --- /dev/null +++ b/e-util/e-table-subset-variable.h @@ -0,0 +1,105 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SUBSET_VARIABLE_H_ +#define _E_TABLE_SUBSET_VARIABLE_H_ + +#include <e-util/e-table-subset.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_SUBSET_VARIABLE \ + (e_table_subset_variable_get_type ()) +#define E_TABLE_SUBSET_VARIABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_SUBSET_VARIABLE, ETableSubsetVariable)) +#define E_TABLE_SUBSET_VARIABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_SUBSET_VARIABLE, ETableSubsetVariableClass)) +#define E_IS_TABLE_SUBSET_VARIABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_SUBSET_VARIABLE)) +#define E_IS_TABLE_SUBSET_VARIABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_SUBSET_VARIABLE)) +#define E_TABLE_SUBSET_VARIABLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_SUBSET_VARIABLE, ETableSubsetVariableClass)) + +G_BEGIN_DECLS + +typedef struct _ETableSubsetVariable ETableSubsetVariable; +typedef struct _ETableSubsetVariableClass ETableSubsetVariableClass; + +struct _ETableSubsetVariable { + ETableSubset parent; + gint n_vals_allocated; +}; + +struct _ETableSubsetVariableClass { + ETableSubsetClass parent_class; + + void (*add) (ETableSubsetVariable *ets, + gint row); + void (*add_array) (ETableSubsetVariable *ets, + const gint *array, + gint count); + void (*add_all) (ETableSubsetVariable *ets); + gboolean (*remove) (ETableSubsetVariable *ets, + gint row); +}; + +GType e_table_subset_variable_get_type + (void) G_GNUC_CONST; +ETableModel * e_table_subset_variable_new (ETableModel *etm); +ETableModel * e_table_subset_variable_construct + (ETableSubsetVariable *etssv, + ETableModel *source); +void e_table_subset_variable_add (ETableSubsetVariable *ets, + gint row); +void e_table_subset_variable_add_array + (ETableSubsetVariable *ets, + const gint *array, + gint count); +void e_table_subset_variable_add_all (ETableSubsetVariable *ets); +gboolean e_table_subset_variable_remove (ETableSubsetVariable *ets, + gint row); +void e_table_subset_variable_clear (ETableSubsetVariable *ets); +void e_table_subset_variable_increment + (ETableSubsetVariable *ets, + gint position, + gint amount); +void e_table_subset_variable_decrement + (ETableSubsetVariable *ets, + gint position, + gint amount); +void e_table_subset_variable_set_allocation + (ETableSubsetVariable *ets, + gint total); + +G_END_DECLS + +#endif /* _E_TABLE_SUBSET_VARIABLE_H_ */ + diff --git a/e-util/e-table-subset.c b/e-util/e-table-subset.c new file mode 100644 index 0000000000..88532d03bd --- /dev/null +++ b/e-util/e-table-subset.c @@ -0,0 +1,567 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> + +#include "e-table-subset.h" + +static void etss_proxy_model_pre_change_real + (ETableSubset *etss, + ETableModel *etm); +static void etss_proxy_model_no_change_real (ETableSubset *etss, + ETableModel *etm); +static void etss_proxy_model_changed_real (ETableSubset *etss, + ETableModel *etm); +static void etss_proxy_model_row_changed_real + (ETableSubset *etss, + ETableModel *etm, + gint row); +static void etss_proxy_model_cell_changed_real + (ETableSubset *etss, + ETableModel *etm, + gint col, + gint row); +static void etss_proxy_model_rows_inserted_real + (ETableSubset *etss, + ETableModel *etm, + gint row, + gint count); +static void etss_proxy_model_rows_deleted_real + (ETableSubset *etss, + ETableModel *etm, + gint row, + gint count); + +#define d(x) + +/* workaround for avoding API breakage */ +#define etss_get_type e_table_subset_get_type +G_DEFINE_TYPE (ETableSubset, etss, E_TYPE_TABLE_MODEL) + +#define ETSS_CLASS(object) (E_TABLE_SUBSET_GET_CLASS(object)) + +#define VALID_ROW(etss, row) (row >= -1 && row < etss->n_map) +#define MAP_ROW(etss, row) (row == -1 ? -1 : etss->map_table[row]) + +static gint +etss_get_view_row (ETableSubset *etss, + gint row) +{ + const gint n = etss->n_map; + const gint * const map_table = etss->map_table; + gint i; + + gint end = MIN (etss->n_map, etss->last_access + 10); + gint start = MAX (0, etss->last_access - 10); + gint initial = MAX (MIN (etss->last_access, end), start); + + for (i = initial; i < end; i++) { + if (map_table[i] == row) { + d (g_print ("a) Found %d from %d\n", i, etss->last_access)); + etss->last_access = i; + return i; + } + } + + for (i = initial - 1; i >= start; i--) { + if (map_table[i] == row) { + d (g_print ("b) Found %d from %d\n", i, etss->last_access)); + etss->last_access = i; + return i; + } + } + + for (i = 0; i < n; i++) { + if (map_table[i] == row) { + d (g_print ("c) Found %d from %d\n", i, etss->last_access)); + etss->last_access = i; + return i; + } + } + return -1; +} + +static void +etss_dispose (GObject *object) +{ + ETableSubset *etss = E_TABLE_SUBSET (object); + + if (etss->source) { + g_signal_handler_disconnect ( + etss->source, + etss->table_model_pre_change_id); + g_signal_handler_disconnect ( + etss->source, + etss->table_model_no_change_id); + g_signal_handler_disconnect ( + etss->source, + etss->table_model_changed_id); + g_signal_handler_disconnect ( + etss->source, + etss->table_model_row_changed_id); + g_signal_handler_disconnect ( + etss->source, + etss->table_model_cell_changed_id); + g_signal_handler_disconnect ( + etss->source, + etss->table_model_rows_inserted_id); + g_signal_handler_disconnect ( + etss->source, + etss->table_model_rows_deleted_id); + + g_object_unref (etss->source); + etss->source = NULL; + + etss->table_model_changed_id = 0; + etss->table_model_row_changed_id = 0; + etss->table_model_cell_changed_id = 0; + etss->table_model_rows_inserted_id = 0; + etss->table_model_rows_deleted_id = 0; + } + + G_OBJECT_CLASS (etss_parent_class)->dispose (object); +} + +static void +etss_finalize (GObject *object) +{ + ETableSubset *etss = E_TABLE_SUBSET (object); + + g_free (etss->map_table); + etss->map_table = NULL; + + G_OBJECT_CLASS (etss_parent_class)->finalize (object); +} + +static gint +etss_column_count (ETableModel *etm) +{ + ETableSubset *etss = (ETableSubset *) etm; + + return e_table_model_column_count (etss->source); +} + +static gint +etss_row_count (ETableModel *etm) +{ + ETableSubset *etss = (ETableSubset *) etm; + + return etss->n_map; +} + +static gpointer +etss_value_at (ETableModel *etm, + gint col, + gint row) +{ + ETableSubset *etss = (ETableSubset *) etm; + + g_return_val_if_fail (VALID_ROW (etss, row), NULL); + + etss->last_access = row; + d (g_print ("g) Setting last_access to %d\n", row)); + return e_table_model_value_at (etss->source, col, MAP_ROW (etss, row)); +} + +static void +etss_set_value_at (ETableModel *etm, + gint col, + gint row, + gconstpointer val) +{ + ETableSubset *etss = (ETableSubset *) etm; + + g_return_if_fail (VALID_ROW (etss, row)); + + etss->last_access = row; + d (g_print ("h) Setting last_access to %d\n", row)); + e_table_model_set_value_at (etss->source, col, MAP_ROW (etss, row), val); +} + +static gboolean +etss_is_cell_editable (ETableModel *etm, + gint col, + gint row) +{ + ETableSubset *etss = (ETableSubset *) etm; + + g_return_val_if_fail (VALID_ROW (etss, row), FALSE); + + return e_table_model_is_cell_editable (etss->source, col, MAP_ROW (etss, row)); +} + +static gboolean +etss_has_save_id (ETableModel *etm) +{ + return TRUE; +} + +static gchar * +etss_get_save_id (ETableModel *etm, + gint row) +{ + ETableSubset *etss = (ETableSubset *) etm; + + g_return_val_if_fail (VALID_ROW (etss, row), NULL); + + if (e_table_model_has_save_id (etss->source)) + return e_table_model_get_save_id (etss->source, MAP_ROW (etss, row)); + else + return g_strdup_printf ("%d", MAP_ROW (etss, row)); +} + +static void +etss_append_row (ETableModel *etm, + ETableModel *source, + gint row) +{ + ETableSubset *etss = (ETableSubset *) etm; + e_table_model_append_row (etss->source, source, row); +} + +static gpointer +etss_duplicate_value (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableSubset *etss = (ETableSubset *) etm; + + return e_table_model_duplicate_value (etss->source, col, value); +} + +static void +etss_free_value (ETableModel *etm, + gint col, + gpointer value) +{ + ETableSubset *etss = (ETableSubset *) etm; + + e_table_model_free_value (etss->source, col, value); +} + +static gpointer +etss_initialize_value (ETableModel *etm, + gint col) +{ + ETableSubset *etss = (ETableSubset *) etm; + + return e_table_model_initialize_value (etss->source, col); +} + +static gboolean +etss_value_is_empty (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableSubset *etss = (ETableSubset *) etm; + + return e_table_model_value_is_empty (etss->source, col, value); +} + +static gchar * +etss_value_to_string (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETableSubset *etss = (ETableSubset *) etm; + + return e_table_model_value_to_string (etss->source, col, value); +} + +static void +etss_class_init (ETableSubsetClass *class) +{ + ETableModelClass *table_class = E_TABLE_MODEL_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = etss_dispose; + object_class->finalize = etss_finalize; + + table_class->column_count = etss_column_count; + table_class->row_count = etss_row_count; + table_class->append_row = etss_append_row; + + table_class->value_at = etss_value_at; + table_class->set_value_at = etss_set_value_at; + table_class->is_cell_editable = etss_is_cell_editable; + + table_class->has_save_id = etss_has_save_id; + table_class->get_save_id = etss_get_save_id; + + table_class->duplicate_value = etss_duplicate_value; + table_class->free_value = etss_free_value; + table_class->initialize_value = etss_initialize_value; + table_class->value_is_empty = etss_value_is_empty; + table_class->value_to_string = etss_value_to_string; + + class->proxy_model_pre_change = etss_proxy_model_pre_change_real; + class->proxy_model_no_change = etss_proxy_model_no_change_real; + class->proxy_model_changed = etss_proxy_model_changed_real; + class->proxy_model_row_changed = etss_proxy_model_row_changed_real; + class->proxy_model_cell_changed = etss_proxy_model_cell_changed_real; + class->proxy_model_rows_inserted = etss_proxy_model_rows_inserted_real; + class->proxy_model_rows_deleted = etss_proxy_model_rows_deleted_real; +} + +static void +etss_init (ETableSubset *etss) +{ + etss->last_access = 0; +} + +static void +etss_proxy_model_pre_change_real (ETableSubset *etss, + ETableModel *etm) +{ + e_table_model_pre_change (E_TABLE_MODEL (etss)); +} + +static void +etss_proxy_model_no_change_real (ETableSubset *etss, + ETableModel *etm) +{ + e_table_model_no_change (E_TABLE_MODEL (etss)); +} + +static void +etss_proxy_model_changed_real (ETableSubset *etss, + ETableModel *etm) +{ + e_table_model_changed (E_TABLE_MODEL (etss)); +} + +static void +etss_proxy_model_row_changed_real (ETableSubset *etss, + ETableModel *etm, + gint row) +{ + gint view_row = etss_get_view_row (etss, row); + if (view_row != -1) + e_table_model_row_changed (E_TABLE_MODEL (etss), view_row); + else + e_table_model_no_change (E_TABLE_MODEL (etss)); +} + +static void +etss_proxy_model_cell_changed_real (ETableSubset *etss, + ETableModel *etm, + gint col, + gint row) +{ + gint view_row = etss_get_view_row (etss, row); + if (view_row != -1) + e_table_model_cell_changed (E_TABLE_MODEL (etss), col, view_row); + else + e_table_model_no_change (E_TABLE_MODEL (etss)); +} + +static void +etss_proxy_model_rows_inserted_real (ETableSubset *etss, + ETableModel *etm, + gint row, + gint count) +{ + e_table_model_no_change (E_TABLE_MODEL (etss)); +} + +static void +etss_proxy_model_rows_deleted_real (ETableSubset *etss, + ETableModel *etm, + gint row, + gint count) +{ + e_table_model_no_change (E_TABLE_MODEL (etss)); +} + +static void +etss_proxy_model_pre_change (ETableModel *etm, + ETableSubset *etss) +{ + if (ETSS_CLASS (etss)->proxy_model_pre_change) + (ETSS_CLASS (etss)->proxy_model_pre_change) (etss, etm); +} + +static void +etss_proxy_model_no_change (ETableModel *etm, + ETableSubset *etss) +{ + if (ETSS_CLASS (etss)->proxy_model_no_change) + (ETSS_CLASS (etss)->proxy_model_no_change) (etss, etm); +} + +static void +etss_proxy_model_changed (ETableModel *etm, + ETableSubset *etss) +{ + if (ETSS_CLASS (etss)->proxy_model_changed) + (ETSS_CLASS (etss)->proxy_model_changed) (etss, etm); +} + +static void +etss_proxy_model_row_changed (ETableModel *etm, + gint row, + ETableSubset *etss) +{ + if (ETSS_CLASS (etss)->proxy_model_row_changed) + (ETSS_CLASS (etss)->proxy_model_row_changed) (etss, etm, row); +} + +static void +etss_proxy_model_cell_changed (ETableModel *etm, + gint col, + gint row, + ETableSubset *etss) +{ + if (ETSS_CLASS (etss)->proxy_model_cell_changed) + (ETSS_CLASS (etss)->proxy_model_cell_changed) (etss, etm, col, row); +} + +static void +etss_proxy_model_rows_inserted (ETableModel *etm, + gint row, + gint col, + ETableSubset *etss) +{ + if (ETSS_CLASS (etss)->proxy_model_rows_inserted) + (ETSS_CLASS (etss)->proxy_model_rows_inserted) (etss, etm, row, col); +} + +static void +etss_proxy_model_rows_deleted (ETableModel *etm, + gint row, + gint col, + ETableSubset *etss) +{ + if (ETSS_CLASS (etss)->proxy_model_rows_deleted) + (ETSS_CLASS (etss)->proxy_model_rows_deleted) (etss, etm, row, col); +} + +ETableModel * +e_table_subset_construct (ETableSubset *etss, + ETableModel *source, + gint nvals) +{ + guint *buffer; + gint i; + + if (nvals) { + buffer = (guint *) g_malloc (sizeof (guint) * nvals); + if (buffer == NULL) + return NULL; + } else + buffer = NULL; + etss->map_table = (gint *) buffer; + etss->n_map = nvals; + etss->source = source; + g_object_ref (source); + + /* Init */ + for (i = 0; i < nvals; i++) + etss->map_table[i] = i; + + etss->table_model_pre_change_id = g_signal_connect ( + source, "model_pre_change", + G_CALLBACK (etss_proxy_model_pre_change), etss); + etss->table_model_no_change_id = g_signal_connect ( + source, "model_no_change", + G_CALLBACK (etss_proxy_model_no_change), etss); + etss->table_model_changed_id = g_signal_connect ( + source, "model_changed", + G_CALLBACK (etss_proxy_model_changed), etss); + etss->table_model_row_changed_id = g_signal_connect ( + source, "model_row_changed", + G_CALLBACK (etss_proxy_model_row_changed), etss); + etss->table_model_cell_changed_id = g_signal_connect ( + source, "model_cell_changed", + G_CALLBACK (etss_proxy_model_cell_changed), etss); + etss->table_model_rows_inserted_id = g_signal_connect ( + source, "model_rows_inserted", + G_CALLBACK (etss_proxy_model_rows_inserted), etss); + etss->table_model_rows_deleted_id = g_signal_connect ( + source, "model_rows_deleted", + G_CALLBACK (etss_proxy_model_rows_deleted), etss); + + return E_TABLE_MODEL (etss); +} + +ETableModel * +e_table_subset_new (ETableModel *source, + const gint nvals) +{ + ETableSubset *etss = g_object_new (E_TYPE_TABLE_SUBSET, NULL); + + if (e_table_subset_construct (etss, source, nvals) == NULL) { + g_object_unref (etss); + return NULL; + } + + return (ETableModel *) etss; +} + +gint +e_table_subset_model_to_view_row (ETableSubset *ets, + gint model_row) +{ + gint i; + for (i = 0; i < ets->n_map; i++) { + if (ets->map_table[i] == model_row) + return i; + } + return -1; +} + +gint +e_table_subset_view_to_model_row (ETableSubset *ets, + gint view_row) +{ + if (view_row >= 0 && view_row < ets->n_map) + return ets->map_table[view_row]; + else + return -1; +} + +ETableModel * +e_table_subset_get_toplevel (ETableSubset *table) +{ + g_return_val_if_fail (table != NULL, NULL); + g_return_val_if_fail (E_IS_TABLE_SUBSET (table), NULL); + + if (E_IS_TABLE_SUBSET (table->source)) + return e_table_subset_get_toplevel (E_TABLE_SUBSET (table->source)); + else + return table->source; +} + +void +e_table_subset_print_debugging (ETableSubset *table_model) +{ + gint i; + for (i = 0; i < table_model->n_map; i++) { + g_print ("%8d\n", table_model->map_table[i]); + } +} diff --git a/e-util/e-table-subset.h b/e-util/e-table-subset.h new file mode 100644 index 0000000000..9e8d69496d --- /dev/null +++ b/e-util/e-table-subset.h @@ -0,0 +1,120 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_SUBSET_H_ +#define _E_TABLE_SUBSET_H_ + +#include <e-util/e-table-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_SUBSET \ + (e_table_subset_get_type ()) +#define E_TABLE_SUBSET(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_SUBSET, ETableSubset)) +#define E_TABLE_SUBSET_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_SUBSET, ETableSubsetClass)) +#define E_IS_TABLE_SUBSET(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_SUBSET)) +#define E_IS_TABLE_SUBSET_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_SUBSET)) +#define E_TABLE_SUBSET_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_SUBSET, ETableSubsetClass)) + +G_BEGIN_DECLS + +typedef struct _ETableSubset ETableSubset; +typedef struct _ETableSubsetClass ETableSubsetClass; + +struct _ETableSubset { + ETableModel parent; + + ETableModel *source; + gint n_map; + gint *map_table; + + gint last_access; + + gint table_model_pre_change_id; + gint table_model_no_change_id; + gint table_model_changed_id; + gint table_model_row_changed_id; + gint table_model_cell_changed_id; + gint table_model_rows_inserted_id; + gint table_model_rows_deleted_id; +}; + +struct _ETableSubsetClass { + ETableModelClass parent_class; + + void (*proxy_model_pre_change) (ETableSubset *etss, + ETableModel *etm); + void (*proxy_model_no_change) (ETableSubset *etss, + ETableModel *etm); + void (*proxy_model_changed) (ETableSubset *etss, + ETableModel *etm); + void (*proxy_model_row_changed) (ETableSubset *etss, + ETableModel *etm, + gint row); + void (*proxy_model_cell_changed) (ETableSubset *etss, + ETableModel *etm, + gint col, + gint row); + void (*proxy_model_rows_inserted) (ETableSubset *etss, + ETableModel *etm, + gint row, + gint count); + void (*proxy_model_rows_deleted) (ETableSubset *etss, + ETableModel *etm, + gint row, + gint count); +}; + +GType e_table_subset_get_type (void) G_GNUC_CONST; +ETableModel * e_table_subset_new (ETableModel *etm, + gint n_vals); +ETableModel * e_table_subset_construct (ETableSubset *ets, + ETableModel *source, + gint nvals); +gint e_table_subset_model_to_view_row + (ETableSubset *ets, + gint model_row); +gint e_table_subset_view_to_model_row + (ETableSubset *ets, + gint view_row); +ETableModel * e_table_subset_get_toplevel (ETableSubset *table_model); +void e_table_subset_print_debugging (ETableSubset *table_model); + +G_END_DECLS + +#endif /* _E_TABLE_SUBSET_H_ */ + diff --git a/e-util/e-table-utils.c b/e-util/e-table-utils.c new file mode 100644 index 0000000000..b914e595b4 --- /dev/null +++ b/e-util/e-table-utils.c @@ -0,0 +1,224 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table-utils.h" + +#include <libintl.h> /* This file uses dgettext() but no _() */ +#include <string.h> + +#include "e-table-header-utils.h" +#include "e-unicode.h" + +ETableHeader * +e_table_state_to_header (GtkWidget *widget, + ETableHeader *full_header, + ETableState *state) +{ + ETableHeader *nh; + const gint max_cols = e_table_header_count (full_header); + gint column; + GValue *val = g_new0 (GValue, 1); + + g_return_val_if_fail (widget, NULL); + g_return_val_if_fail (full_header, NULL); + g_return_val_if_fail (state, NULL); + + nh = e_table_header_new (); + g_value_init (val, G_TYPE_DOUBLE); + g_value_set_double (val, e_table_header_width_extras (widget)); + g_object_set_property (G_OBJECT (nh), "width_extras", val); + g_free (val); + + for (column = 0; column < state->col_count; column++) { + gint col; + gdouble expansion; + ETableCol *table_col; + + col = state->columns[column]; + expansion = state->expansions[column]; + + if (col >= max_cols) + continue; + + table_col = e_table_header_get_column (full_header, col); + + if (expansion >= -1) + table_col->expansion = expansion; + + e_table_header_add_column (nh, table_col, -1); + } + + return nh; +} + +static ETableCol * +et_col_spec_to_col (ETableColumnSpecification *col_spec, + ETableExtras *ete, + const gchar *domain) +{ + ETableCol *col = NULL; + ECell *cell = NULL; + GCompareDataFunc compare = NULL; + ETableSearchFunc search = NULL; + + if (col_spec->cell) + cell = e_table_extras_get_cell (ete, col_spec->cell); + if (col_spec->compare) + compare = e_table_extras_get_compare (ete, col_spec->compare); + if (col_spec->search) + search = e_table_extras_get_search (ete, col_spec->search); + + if (cell && compare) { + gchar *title = dgettext (domain, col_spec->title); + + title = g_strdup (title); + + if (col_spec->pixbuf && *col_spec->pixbuf) { + const gchar *icon_name; + + icon_name = e_table_extras_get_icon_name ( + ete, col_spec->pixbuf); + if (icon_name != NULL) { + col = e_table_col_new ( + col_spec->model_col, + title, icon_name, + col_spec->expansion, + col_spec->minimum_width, + cell, compare, + col_spec->resizable, + col_spec->disabled, + col_spec->priority); + } + } + + if (col == NULL && col_spec->title && *col_spec->title) { + col = e_table_col_new ( + col_spec->model_col, title, NULL, + col_spec->expansion, + col_spec->minimum_width, + cell, compare, + col_spec->resizable, + col_spec->disabled, + col_spec->priority); + } + + if (col) { + col->search = search; + if (col_spec->sortable && !strcmp (col_spec->sortable, "false")) + col->sortable = FALSE; + else + col->sortable = TRUE; + } + g_free (title); + } + if (col && col_spec->compare_col != col_spec->model_col) + g_object_set ( + col, + "compare_col", col_spec->compare_col, + NULL); + return col; +} + +ETableHeader * +e_table_spec_to_full_header (ETableSpecification *spec, + ETableExtras *ete) +{ + ETableHeader *nh; + gint column; + + g_return_val_if_fail (spec, NULL); + g_return_val_if_fail (ete, NULL); + + nh = e_table_header_new (); + + for (column = 0; spec->columns[column]; column++) { + ETableCol *col = et_col_spec_to_col ( + spec->columns[column], ete, spec->domain); + + if (col) { + e_table_header_add_column (nh, col, -1); + g_object_unref (col); + } + } + + return nh; +} + +static gboolean +check_col (ETableCol *col, + gpointer user_data) +{ + return col->search ? TRUE : FALSE; +} + +ETableCol * +e_table_util_calculate_current_search_col (ETableHeader *header, + ETableHeader *full_header, + ETableSortInfo *sort_info, + gboolean always_search) +{ + gint i; + gint count; + ETableCol *col = NULL; + + count = e_table_sort_info_grouping_get_count (sort_info); + + for (i = 0; i < count; i++) { + ETableSortColumn column; + + column = e_table_sort_info_grouping_get_nth (sort_info, i); + + col = e_table_header_get_column (full_header, column.column); + + if (col && col->search) + break; + + col = NULL; + } + + if (col == NULL) { + count = e_table_sort_info_sorting_get_count (sort_info); + for (i = 0; i < count; i++) { + ETableSortColumn column; + + column = e_table_sort_info_sorting_get_nth (sort_info, i); + + col = e_table_header_get_column (full_header, column.column); + + if (col && col->search) + break; + + col = NULL; + } + } + + if (col == NULL && always_search) { + col = e_table_header_prioritized_column_selected (header, check_col, NULL); + } + + return col; +} diff --git a/e-util/e-table-utils.h b/e-util/e-table-utils.h new file mode 100644 index 0000000000..1b7b144ce5 --- /dev/null +++ b/e-util/e-table-utils.h @@ -0,0 +1,54 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_UTILS_H_ +#define _E_TABLE_UTILS_H_ + +#include <e-util/e-table-extras.h> +#include <e-util/e-table-header.h> +#include <e-util/e-table-specification.h> +#include <e-util/e-table-state.h> + +G_BEGIN_DECLS + +ETableHeader * e_table_state_to_header (GtkWidget *widget, + ETableHeader *full_header, + ETableState *state); + +ETableHeader * e_table_spec_to_full_header (ETableSpecification *spec, + ETableExtras *ete); + +ETableCol * e_table_util_calculate_current_search_col + (ETableHeader *header, + ETableHeader *full_header, + ETableSortInfo *sort_info, + gboolean always_search); + +G_END_DECLS + +#endif /* _E_TABLE_UTILS_H_ */ + diff --git a/e-util/e-table-without.c b/e-util/e-table-without.c new file mode 100644 index 0000000000..7139ad15df --- /dev/null +++ b/e-util/e-table-without.c @@ -0,0 +1,412 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <string.h> + +#include "e-table-without.h" + +#define E_TABLE_WITHOUT_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TABLE_WITHOUT, ETableWithoutPrivate)) + +/* workaround for avoiding API breakage */ +#define etw_get_type e_table_without_get_type +G_DEFINE_TYPE (ETableWithout, etw, E_TYPE_TABLE_SUBSET) + +#define INCREMENT_AMOUNT 10 + +struct _ETableWithoutPrivate { + GHashTable *hash; + + GHashFunc hash_func; + GCompareFunc compare_func; + + ETableWithoutGetKeyFunc get_key_func; + ETableWithoutDuplicateKeyFunc duplicate_key_func; + ETableWithoutFreeKeyFunc free_gotten_key_func; + ETableWithoutFreeKeyFunc free_duplicated_key_func; + + gpointer closure; +}; + +static gboolean +check (ETableWithout *etw, + gint model_row) +{ + gboolean ret_val; + gpointer key; + ETableSubset *etss = E_TABLE_SUBSET (etw); + + if (etw->priv->get_key_func) + key = etw->priv->get_key_func (etss->source, model_row, etw->priv->closure); + else + key = GINT_TO_POINTER (model_row); + ret_val = (g_hash_table_lookup (etw->priv->hash, key) != NULL); + if (etw->priv->free_gotten_key_func) + etw->priv->free_gotten_key_func (key, etw->priv->closure); + return ret_val; +} + +static gboolean +check_with_key (ETableWithout *etw, + gpointer key, + gint model_row) +{ + gboolean ret_val; + gpointer key2; + ETableSubset *etss = E_TABLE_SUBSET (etw); + + if (etw->priv->get_key_func) + key2 = etw->priv->get_key_func (etss->source, model_row, etw->priv->closure); + else + key2 = GINT_TO_POINTER (model_row); + if (etw->priv->compare_func) + ret_val = (etw->priv->compare_func (key, key2)); + else + ret_val = (key == key2); + if (etw->priv->free_gotten_key_func) + etw->priv->free_gotten_key_func (key2, etw->priv->closure); + return ret_val; +} + +static gint +etw_view_to_model_row (ETableWithout *etw, + gint view_row) +{ + ETableSubset *etss = E_TABLE_SUBSET (etw); + return etss->map_table[view_row]; +} + +static void +add_row (ETableWithout *etw, + gint model_row) +{ + ETableSubset *etss = E_TABLE_SUBSET (etw); + + e_table_model_pre_change (E_TABLE_MODEL (etw)); + + etss->map_table = g_renew (int, etss->map_table, etss->n_map + 1); + + etss->map_table[etss->n_map++] = model_row; + + e_table_model_row_inserted (E_TABLE_MODEL (etw), etss->n_map - 1); +} + +static void +remove_row (ETableWithout *etw, + gint view_row) +{ + ETableSubset *etss = E_TABLE_SUBSET (etw); + + e_table_model_pre_change (E_TABLE_MODEL (etw)); + memmove ( + etss->map_table + view_row, + etss->map_table + view_row + 1, + (etss->n_map - view_row - 1) * sizeof (gint)); + etss->n_map--; + e_table_model_row_deleted (E_TABLE_MODEL (etw), view_row); +} + +static void +delete_hash_element (gpointer key, + gpointer value, + gpointer closure) +{ + ETableWithout *etw = closure; + if (etw->priv->free_duplicated_key_func) + etw->priv->free_duplicated_key_func (key, etw->priv->closure); +} + +static void +etw_dispose (GObject *object) +{ + ETableWithoutPrivate *priv; + + priv = E_TABLE_WITHOUT_GET_PRIVATE (object); + + if (priv->hash != NULL) { + g_hash_table_foreach (priv->hash, delete_hash_element, object); + g_hash_table_destroy (priv->hash); + priv->hash = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (etw_parent_class)->dispose (object); +} + +static void +etw_proxy_model_rows_inserted (ETableSubset *etss, + ETableModel *etm, + gint model_row, + gint count) +{ + gint i; + ETableWithout *etw = E_TABLE_WITHOUT (etss); + gboolean shift = FALSE; + + /* i is View row */ + if (model_row != etss->n_map) { + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] > model_row) + etss->map_table[i] += count; + } + shift = TRUE; + } + + /* i is Model row */ + for (i = model_row; i < model_row + count; i++) { + if (!check (etw, i)) { + add_row (etw, i); + } + } + if (shift) + e_table_model_changed (E_TABLE_MODEL (etw)); + else + e_table_model_no_change (E_TABLE_MODEL (etw)); +} + +static void +etw_proxy_model_rows_deleted (ETableSubset *etss, + ETableModel *etm, + gint model_row, + gint count) +{ + gint i; /* View row */ + ETableWithout *etw = E_TABLE_WITHOUT (etss); + gboolean shift = FALSE; + + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] >= model_row && + etss->map_table[i] < model_row + count) { + remove_row (etw, i); + i--; + } else if (etss->map_table[i] >= model_row + count) { + etss->map_table[i] -= count; + shift = TRUE; + } + } + if (shift) + e_table_model_changed (E_TABLE_MODEL (etw)); + else + e_table_model_no_change (E_TABLE_MODEL (etw)); +} + +static void +etw_proxy_model_changed (ETableSubset *etss, + ETableModel *etm) +{ + gint i; /* Model row */ + gint j; /* View row */ + gint row_count; + ETableWithout *etw = E_TABLE_WITHOUT (etss); + + g_free (etss->map_table); + row_count = e_table_model_row_count (etm); + etss->map_table = g_new (int, row_count); + + for (i = 0, j = 0; i < row_count; i++) { + if (!check (etw, i)) { + etss->map_table[j++] = i; + } + } + etss->n_map = j; + + if (E_TABLE_SUBSET_CLASS (etw_parent_class)->proxy_model_changed) + E_TABLE_SUBSET_CLASS (etw_parent_class)->proxy_model_changed (etss, etm); +} + +static void +etw_class_init (ETableWithoutClass *class) +{ + GObjectClass *object_class; + ETableSubsetClass *etss_class; + + g_type_class_add_private (class, sizeof (ETableWithoutPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = etw_dispose; + + etss_class = E_TABLE_SUBSET_CLASS (class); + etss_class->proxy_model_rows_inserted = etw_proxy_model_rows_inserted; + etss_class->proxy_model_rows_deleted = etw_proxy_model_rows_deleted; + etss_class->proxy_model_changed = etw_proxy_model_changed; +} + +static void +etw_init (ETableWithout *etw) +{ + etw->priv = E_TABLE_WITHOUT_GET_PRIVATE (etw); +} + +ETableModel * +e_table_without_construct (ETableWithout *etw, + ETableModel *source, + GHashFunc hash_func, + GCompareFunc compare_func, + ETableWithoutGetKeyFunc get_key_func, + ETableWithoutDuplicateKeyFunc duplicate_key_func, + ETableWithoutFreeKeyFunc free_gotten_key_func, + ETableWithoutFreeKeyFunc free_duplicated_key_func, + gpointer closure) +{ + if (e_table_subset_construct (E_TABLE_SUBSET (etw), source, 1) == NULL) + return NULL; + E_TABLE_SUBSET (etw)->n_map = 0; + + etw->priv->hash_func = hash_func; + etw->priv->compare_func = compare_func; + etw->priv->get_key_func = get_key_func; + etw->priv->duplicate_key_func = duplicate_key_func; + etw->priv->free_gotten_key_func = free_gotten_key_func; + etw->priv->free_duplicated_key_func = free_duplicated_key_func; + etw->priv->closure = closure; + + etw->priv->hash = g_hash_table_new ( + etw->priv->hash_func, etw->priv->compare_func); + + return E_TABLE_MODEL (etw); +} + +ETableModel * +e_table_without_new (ETableModel *source, + GHashFunc hash_func, + GCompareFunc compare_func, + ETableWithoutGetKeyFunc get_key_func, + ETableWithoutDuplicateKeyFunc duplicate_key_func, + ETableWithoutFreeKeyFunc free_gotten_key_func, + ETableWithoutFreeKeyFunc free_duplicated_key_func, + gpointer closure) +{ + ETableWithout *etw = g_object_new (E_TYPE_TABLE_WITHOUT, NULL); + + if (e_table_without_construct (etw, + source, + hash_func, + compare_func, + get_key_func, + duplicate_key_func, + free_gotten_key_func, + free_duplicated_key_func, + closure) + == NULL) { + g_object_unref (etw); + return NULL; + } + + return (ETableModel *) etw; +} + +void +e_table_without_hide (ETableWithout *etw, + gpointer key) +{ + gint i; /* View row */ + ETableSubset *etss = E_TABLE_SUBSET (etw); + + if (etw->priv->duplicate_key_func) + key = etw->priv->duplicate_key_func (key, etw->priv->closure); + + g_hash_table_insert (etw->priv->hash, key, key); + for (i = 0; i < etss->n_map; i++) { + if (check_with_key (etw, key, etw_view_to_model_row (etw, i))) { + remove_row (etw, i); + i--; + } + } +} + +/* An adopted key will later be freed using the free_duplicated_key function. */ +void +e_table_without_hide_adopt (ETableWithout *etw, + gpointer key) +{ + gint i; /* View row */ + ETableSubset *etss = E_TABLE_SUBSET (etw); + + g_hash_table_insert (etw->priv->hash, key, key); + for (i = 0; i < etss->n_map; i++) { + if (check_with_key (etw, key, etw_view_to_model_row (etw, i))) { + remove_row (etw, i); + i--; + } + } +} + +void +e_table_without_show (ETableWithout *etw, + gpointer key) +{ + gint i; /* Model row */ + ETableSubset *etss = E_TABLE_SUBSET (etw); + gint count; + gpointer old_key; + + count = e_table_model_row_count (etss->source); + + for (i = 0; i < count; i++) { + if (check_with_key (etw, key, i)) { + add_row (etw, i); + } + } + if (g_hash_table_lookup_extended (etw->priv->hash, key, &old_key, NULL)) { +#if 0 + if (etw->priv->free_duplicated_key_func) + etw->priv->free_duplicated_key_func (key, etw->priv->closure); +#endif + g_hash_table_remove (etw->priv->hash, key); + } +} + +void +e_table_without_show_all (ETableWithout *etw) +{ + gint i; /* Model row */ + gint row_count; + ETableSubset *etss = E_TABLE_SUBSET (etw); + + e_table_model_pre_change (E_TABLE_MODEL (etw)); + + if (etw->priv->hash) { + g_hash_table_foreach (etw->priv->hash, delete_hash_element, etw); + g_hash_table_destroy (etw->priv->hash); + etw->priv->hash = NULL; + } + etw->priv->hash = g_hash_table_new ( + etw->priv->hash_func, etw->priv->compare_func); + + row_count = e_table_model_row_count (E_TABLE_MODEL (etss->source)); + g_free (etss->map_table); + etss->map_table = g_new (int, row_count); + + for (i = 0; i < row_count; i++) { + etss->map_table[i] = i; + } + etss->n_map = row_count; + + e_table_model_changed (E_TABLE_MODEL (etw)); +} diff --git a/e-util/e-table-without.h b/e-util/e-table-without.h new file mode 100644 index 0000000000..0853c54cb5 --- /dev/null +++ b/e-util/e-table-without.h @@ -0,0 +1,104 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_WITHOUT_H_ +#define _E_TABLE_WITHOUT_H_ + +#include <e-util/e-table-subset.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE_WITHOUT \ + (e_table_without_get_type ()) +#define E_TABLE_WITHOUT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE_WITHOUT, ETableWithout)) +#define E_TABLE_WITHOUT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE_WITHOUT, ETableWithoutClass)) +#define E_IS_TABLE_WITHOUT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE_WITHOUT)) +#define E_IS_TABLE_WITHOUT_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE_WITHOUT)) +#define E_TABLE_WITHOUT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE_WITHOUT, ETableWithoutClass)) + +G_BEGIN_DECLS + +typedef struct _ETableWithout ETableWithout; +typedef struct _ETableWithoutClass ETableWithoutClass; +typedef struct _ETableWithoutPrivate ETableWithoutPrivate; + +typedef gpointer (*ETableWithoutGetKeyFunc) (ETableModel *source, + gint row, + gpointer closure); +typedef gpointer (*ETableWithoutDuplicateKeyFunc)(gconstpointer key, + gpointer closure); +typedef void (*ETableWithoutFreeKeyFunc) (gpointer key, + gpointer closure); + +struct _ETableWithout { + ETableSubset parent; + ETableWithoutPrivate *priv; +}; + +struct _ETableWithoutClass { + ETableSubsetClass parent_class; +}; + +GType e_table_without_get_type (void) G_GNUC_CONST; +ETableModel * e_table_without_new (ETableModel *source, + GHashFunc hash_func, + GCompareFunc compare_func, + ETableWithoutGetKeyFunc get_key_func, + ETableWithoutDuplicateKeyFunc duplicate_key_func, + ETableWithoutFreeKeyFunc free_gotten_key_func, + ETableWithoutFreeKeyFunc free_duplicated_key_func, + gpointer closure); +ETableModel * e_table_without_construct (ETableWithout *etw, + ETableModel *source, + GHashFunc hash_func, + GCompareFunc compare_func, + ETableWithoutGetKeyFunc get_key_func, + ETableWithoutDuplicateKeyFunc duplicate_key_func, + ETableWithoutFreeKeyFunc free_gotten_key_func, + ETableWithoutFreeKeyFunc free_duplicated_key_func, + gpointer closure); +void e_table_without_hide (ETableWithout *etw, + gpointer key); +void e_table_without_hide_adopt (ETableWithout *etw, + gpointer key); +void e_table_without_show (ETableWithout *etw, + gpointer key); +void e_table_without_show_all (ETableWithout *etw); + +G_END_DECLS + +#endif /* _E_TABLE_WITHOUT_H_ */ + diff --git a/e-util/e-table.c b/e-util/e-table.c new file mode 100644 index 0000000000..4dc90bebf1 --- /dev/null +++ b/e-util/e-table.c @@ -0,0 +1,3626 @@ +/* + * e-table.c - A graphical view of a Table. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-table.h" + +#include <stdlib.h> +#include <string.h> +#include <stdio.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <gdk/gdkkeysyms.h> + +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-canvas-background.h" +#include "e-canvas-vbox.h" +#include "e-canvas.h" +#include "e-table-click-to-add.h" +#include "e-table-column-specification.h" +#include "e-table-group-leaf.h" +#include "e-table-header-item.h" +#include "e-table-header-utils.h" +#include "e-table-subset.h" +#include "e-table-utils.h" +#include "e-unicode.h" +#include "gal-a11y-e-table.h" + +#define COLUMN_HEADER_HEIGHT 16 + +#define d(x) + +#if d(!)0 +#define e_table_item_leave_edit_(x) \ + (e_table_item_leave_edit ((x)), \ + g_print ("%s: e_table_item_leave_edit\n", __FUNCTION__)) +#else +#define e_table_item_leave_edit_(x) (e_table_item_leave_edit((x))) +#endif + +enum { + CURSOR_CHANGE, + CURSOR_ACTIVATED, + SELECTION_CHANGE, + DOUBLE_CLICK, + RIGHT_CLICK, + CLICK, + KEY_PRESS, + START_DRAG, + STATE_CHANGE, + WHITE_SPACE_EVENT, + + CUT_CLIPBOARD, + COPY_CLIPBOARD, + PASTE_CLIPBOARD, + SELECT_ALL, + + TABLE_DRAG_BEGIN, + TABLE_DRAG_END, + TABLE_DRAG_DATA_GET, + TABLE_DRAG_DATA_DELETE, + + TABLE_DRAG_LEAVE, + TABLE_DRAG_MOTION, + TABLE_DRAG_DROP, + TABLE_DRAG_DATA_RECEIVED, + + LAST_SIGNAL +}; + +enum { + PROP_0, + PROP_LENGTH_THRESHOLD, + PROP_MODEL, + PROP_UNIFORM_ROW_HEIGHT, + PROP_ALWAYS_SEARCH, + PROP_USE_CLICK_TO_ADD, + PROP_HADJUSTMENT, + PROP_VADJUSTMENT, + PROP_HSCROLL_POLICY, + PROP_VSCROLL_POLICY +}; + +enum { + ET_SCROLL_UP = 1 << 0, + ET_SCROLL_DOWN = 1 << 1, + ET_SCROLL_LEFT = 1 << 2, + ET_SCROLL_RIGHT = 1 << 3 +}; + +static guint et_signals[LAST_SIGNAL] = { 0 }; + +static void e_table_fill_table (ETable *e_table, ETableModel *model); +static gboolean changed_idle (gpointer data); + +static void et_grab_focus (GtkWidget *widget); + +static void et_drag_begin (GtkWidget *widget, + GdkDragContext *context, + ETable *et); +static void et_drag_end (GtkWidget *widget, + GdkDragContext *context, + ETable *et); +static void et_drag_data_get (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time, + ETable *et); +static void et_drag_data_delete (GtkWidget *widget, + GdkDragContext *context, + ETable *et); + +static void et_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time, + ETable *et); +static gboolean et_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETable *et); +static gboolean et_drag_drop (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETable *et); +static void et_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time, + ETable *et); + +static gint et_focus (GtkWidget *container, GtkDirectionType direction); + +static void scroll_off (ETable *et); +static void scroll_on (ETable *et, guint scroll_direction); + +G_DEFINE_TYPE_WITH_CODE (ETable, e_table, GTK_TYPE_TABLE, + G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL)) + +static void +et_disconnect_model (ETable *et) +{ + if (et->model == NULL) + return; + + if (et->table_model_change_id != 0) + g_signal_handler_disconnect ( + et->model, et->table_model_change_id); + if (et->table_row_change_id != 0) + g_signal_handler_disconnect ( + et->model, et->table_row_change_id); + if (et->table_cell_change_id != 0) + g_signal_handler_disconnect ( + et->model, et->table_cell_change_id); + if (et->table_rows_inserted_id != 0) + g_signal_handler_disconnect ( + et->model, et->table_rows_inserted_id); + if (et->table_rows_deleted_id != 0) + g_signal_handler_disconnect ( + et->model, et->table_rows_deleted_id); + + et->table_model_change_id = 0; + et->table_row_change_id = 0; + et->table_cell_change_id = 0; + et->table_rows_inserted_id = 0; + et->table_rows_deleted_id = 0; +} + +static void +e_table_state_change (ETable *et) +{ + if (et->state_change_freeze) + et->state_changed = TRUE; + else + g_signal_emit (et, et_signals[STATE_CHANGE], 0); +} + +#define CHECK_HORIZONTAL(et) \ + if ((et)->horizontal_scrolling || (et)->horizontal_resize) \ + e_table_header_update_horizontal (et->header); + +static void +clear_current_search_col (ETable *et) +{ + et->search_col_set = FALSE; +} + +static ETableCol * +current_search_col (ETable *et) +{ + if (!et->search_col_set) { + et->current_search_col = + e_table_util_calculate_current_search_col ( + et->header, + et->full_header, + et->sort_info, + et->always_search); + et->search_col_set = TRUE; + } + + return et->current_search_col; +} + +static void +et_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + ETable *et = E_TABLE (widget); + + GTK_WIDGET_CLASS (e_table_parent_class)-> + get_preferred_width (widget, minimum, natural); + + if (et->horizontal_resize) { + *minimum = MAX (*minimum, et->header_width); + *natural = MAX (*natural, et->header_width); + } +} + +static void +et_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + GTK_WIDGET_CLASS (e_table_parent_class)-> + get_preferred_height (widget, minimum, natural); +} + +static void +set_header_width (ETable *et) +{ + if (et->horizontal_resize) { + et->header_width = e_table_header_min_width (et->header); + gtk_widget_queue_resize (GTK_WIDGET (et)); + } +} + +static void +structure_changed (ETableHeader *header, + ETable *et) +{ + e_table_state_change (et); + set_header_width (et); + clear_current_search_col (et); +} + +static void +expansion_changed (ETableHeader *header, + ETable *et) +{ + e_table_state_change (et); + set_header_width (et); +} + +static void +dimension_changed (ETableHeader *header, + gint total_width, + ETable *et) +{ + set_header_width (et); +} + +static void +disconnect_header (ETable *e_table) +{ + if (e_table->header == NULL) + return; + + if (e_table->structure_change_id) + g_signal_handler_disconnect ( + e_table->header, e_table->structure_change_id); + if (e_table->expansion_change_id) + g_signal_handler_disconnect ( + e_table->header, e_table->expansion_change_id); + if (e_table->dimension_change_id) + g_signal_handler_disconnect ( + e_table->header, e_table->dimension_change_id); + + g_object_unref (e_table->header); + e_table->header = NULL; +} + +static void +connect_header (ETable *e_table, + ETableState *state) +{ + if (e_table->header != NULL) + disconnect_header (e_table); + + e_table->header = e_table_state_to_header ( + GTK_WIDGET (e_table), e_table->full_header, state); + + e_table->structure_change_id = g_signal_connect ( + e_table->header, "structure_change", + G_CALLBACK (structure_changed), e_table); + e_table->expansion_change_id = g_signal_connect ( + e_table->header, "expansion_change", + G_CALLBACK (expansion_changed), e_table); + e_table->dimension_change_id = g_signal_connect ( + e_table->header, "dimension_change", + G_CALLBACK (dimension_changed), e_table); +} + +static void +et_dispose (GObject *object) +{ + ETable *et = E_TABLE (object); + + et_disconnect_model (et); + + if (et->search) { + if (et->search_search_id) + g_signal_handler_disconnect ( + et->search, et->search_search_id); + if (et->search_accept_id) + g_signal_handler_disconnect ( + et->search, et->search_accept_id); + g_object_unref (et->search); + et->search = NULL; + } + + if (et->group_info_change_id) { + g_signal_handler_disconnect ( + et->sort_info, et->group_info_change_id); + et->group_info_change_id = 0; + } + + if (et->sort_info_change_id) { + g_signal_handler_disconnect ( + et->sort_info, et->sort_info_change_id); + et->sort_info_change_id = 0; + } + + if (et->reflow_idle_id) { + g_source_remove (et->reflow_idle_id); + et->reflow_idle_id = 0; + } + + scroll_off (et); + + disconnect_header (et); + + if (et->model) { + g_object_unref (et->model); + et->model = NULL; + } + + if (et->full_header) { + g_object_unref (et->full_header); + et->full_header = NULL; + } + + if (et->sort_info) { + g_object_unref (et->sort_info); + et->sort_info = NULL; + } + + if (et->sorter) { + g_object_unref (et->sorter); + et->sorter = NULL; + } + + if (et->selection) { + g_object_unref (et->selection); + et->selection = NULL; + } + + if (et->spec) { + g_object_unref (et->spec); + et->spec = NULL; + } + + if (et->header_canvas != NULL) { + gtk_widget_destroy (GTK_WIDGET (et->header_canvas)); + et->header_canvas = NULL; + } + + if (et->site != NULL) { + e_table_drag_source_unset (et); + et->site = NULL; + } + + if (et->table_canvas != NULL) { + gtk_widget_destroy (GTK_WIDGET (et->table_canvas)); + et->table_canvas = NULL; + } + + if (et->rebuild_idle_id != 0) { + g_source_remove (et->rebuild_idle_id); + et->rebuild_idle_id = 0; + } + + g_free (et->click_to_add_message); + et->click_to_add_message = NULL; + + g_free (et->domain); + et->domain = NULL; + + G_OBJECT_CLASS (e_table_parent_class)->dispose (object); +} + +static void +et_unrealize (GtkWidget *widget) +{ + scroll_off (E_TABLE (widget)); + + if (GTK_WIDGET_CLASS (e_table_parent_class)->unrealize) + GTK_WIDGET_CLASS (e_table_parent_class)->unrealize (widget); +} + +static gboolean +check_row (ETable *et, + gint model_row, + gint col, + ETableSearchFunc search, + gchar *string) +{ + gconstpointer value; + + value = e_table_model_value_at (et->model, col, model_row); + + return search (value, string); +} + +static gboolean +et_search_search (ETableSearch *search, + gchar *string, + ETableSearchFlags flags, + ETable *et) +{ + gint cursor; + gint rows; + gint i; + ETableCol *col = current_search_col (et); + + if (col == NULL) + return FALSE; + + rows = e_table_model_row_count (et->model); + + g_object_get ( + et->selection, + "cursor_row", &cursor, + NULL); + + if ((flags & E_TABLE_SEARCH_FLAGS_CHECK_CURSOR_FIRST) && + cursor < rows && cursor >= 0 && + check_row (et, cursor, col->col_idx, col->search, string)) + return TRUE; + + cursor = e_sorter_model_to_sorted (E_SORTER (et->sorter), cursor); + + for (i = cursor + 1; i < rows; i++) { + gint model_row = e_sorter_sorted_to_model (E_SORTER (et->sorter), i); + if (check_row (et, model_row, col->col_idx, col->search, string)) { + e_selection_model_select_as_key_press ( + E_SELECTION_MODEL (et->selection), + model_row, col->col_idx, GDK_CONTROL_MASK); + return TRUE; + } + } + + for (i = 0; i < cursor; i++) { + gint model_row = e_sorter_sorted_to_model (E_SORTER (et->sorter), i); + if (check_row (et, model_row, col->col_idx, col->search, string)) { + e_selection_model_select_as_key_press ( + E_SELECTION_MODEL (et->selection), + model_row, col->col_idx, GDK_CONTROL_MASK); + return TRUE; + } + } + + cursor = e_sorter_sorted_to_model (E_SORTER (et->sorter), cursor); + + /* Check if the cursor row is the only matching row. */ + return (!(flags & E_TABLE_SEARCH_FLAGS_CHECK_CURSOR_FIRST) && + cursor < rows && cursor >= 0 && + check_row (et, cursor, col->col_idx, col->search, string)); +} + +static void +et_search_accept (ETableSearch *search, + ETable *et) +{ + gint cursor; + ETableCol *col = current_search_col (et); + + if (col == NULL) + return; + + g_object_get (et->selection, "cursor_row", &cursor, NULL); + + e_selection_model_select_as_key_press ( + E_SELECTION_MODEL (et->selection), cursor, col->col_idx, 0); +} + +static void +init_search (ETable *e_table) +{ + if (e_table->search != NULL) + return; + + e_table->search = e_table_search_new (); + + e_table->search_search_id = g_signal_connect ( + e_table->search, "search", + G_CALLBACK (et_search_search), e_table); + e_table->search_accept_id = g_signal_connect ( + e_table->search, "accept", + G_CALLBACK (et_search_accept), e_table); +} + +static void +et_finalize (GObject *object) +{ + ETable *et = E_TABLE (object); + + g_free (et->click_to_add_message); + et->click_to_add_message = NULL; + + g_free (et->domain); + et->domain = NULL; + + G_OBJECT_CLASS (e_table_parent_class)->finalize (object); +} + +static void +e_table_init (ETable *e_table) +{ + gtk_widget_set_can_focus (GTK_WIDGET (e_table), TRUE); + + gtk_table_set_homogeneous (GTK_TABLE (e_table), FALSE); + + e_table->sort_info = NULL; + e_table->group_info_change_id = 0; + e_table->sort_info_change_id = 0; + e_table->structure_change_id = 0; + e_table->expansion_change_id = 0; + e_table->dimension_change_id = 0; + e_table->reflow_idle_id = 0; + e_table->scroll_idle_id = 0; + + e_table->alternating_row_colors = 1; + e_table->horizontal_draw_grid = 1; + e_table->vertical_draw_grid = 1; + e_table->draw_focus = 1; + e_table->cursor_mode = E_CURSOR_SIMPLE; + e_table->length_threshold = 200; + e_table->uniform_row_height = FALSE; + + e_table->need_rebuild = 0; + e_table->rebuild_idle_id = 0; + + e_table->horizontal_scrolling = FALSE; + e_table->horizontal_resize = FALSE; + + e_table->click_to_add_message = NULL; + e_table->domain = NULL; + + e_table->drop_row = -1; + e_table->drop_col = -1; + e_table->site = NULL; + + e_table->do_drag = 0; + + e_table->sorter = NULL; + e_table->selection = e_table_selection_model_new (); + e_table->cursor_loc = E_TABLE_CURSOR_LOC_NONE; + e_table->spec = NULL; + + e_table->always_search = g_getenv ("GAL_ALWAYS_SEARCH") ? TRUE : FALSE; + + e_table->search = NULL; + e_table->search_search_id = 0; + e_table->search_accept_id = 0; + + e_table->current_search_col = NULL; + + e_table->header_width = 0; + + e_table->state_changed = FALSE; + e_table->state_change_freeze = 0; +} + +/* Grab_focus handler for the ETable */ +static void +et_grab_focus (GtkWidget *widget) +{ + ETable *e_table; + + e_table = E_TABLE (widget); + + gtk_widget_grab_focus (GTK_WIDGET (e_table->table_canvas)); +} + +/* Focus handler for the ETable */ +static gint +et_focus (GtkWidget *container, + GtkDirectionType direction) +{ + ETable *e_table; + + e_table = E_TABLE (container); + + if (gtk_container_get_focus_child (GTK_CONTAINER (container))) { + gtk_container_set_focus_child (GTK_CONTAINER (container), NULL); + return FALSE; + } + + return gtk_widget_child_focus (GTK_WIDGET (e_table->table_canvas), direction); +} + +static void +set_header_canvas_width (ETable *e_table) +{ + gdouble oldwidth, oldheight, width; + + if (!(e_table->header_item && e_table->header_canvas && e_table->table_canvas)) + return; + + gnome_canvas_get_scroll_region ( + GNOME_CANVAS (e_table->table_canvas), + NULL, NULL, &width, NULL); + gnome_canvas_get_scroll_region ( + GNOME_CANVAS (e_table->header_canvas), + NULL, NULL, &oldwidth, &oldheight); + + if (oldwidth != width || + oldheight != E_TABLE_HEADER_ITEM (e_table->header_item)->height - 1) + gnome_canvas_set_scroll_region ( + GNOME_CANVAS (e_table->header_canvas), + 0, 0, width, /* COLUMN_HEADER_HEIGHT - 1 */ + E_TABLE_HEADER_ITEM (e_table->header_item)->height - 1); + +} + +static void +header_canvas_size_allocate (GtkWidget *widget, + GtkAllocation *alloc, + ETable *e_table) +{ + GtkAllocation allocation; + + set_header_canvas_width (e_table); + + gtk_widget_get_allocation ( + GTK_WIDGET (e_table->header_canvas), &allocation); + + /* When the header item is created ->height == 0, + * as the font is only created when everything is realized. + * So we set the usize here as well, so that the size of the + * header is correct */ + if (allocation.height != E_TABLE_HEADER_ITEM (e_table->header_item)->height) + g_object_set ( + e_table->header_canvas, "height-request", + E_TABLE_HEADER_ITEM (e_table->header_item)->height, + NULL); +} + +static void +group_info_changed (ETableSortInfo *info, + ETable *et) +{ + gboolean will_be_grouped = e_table_sort_info_grouping_get_count (info) > 0; + clear_current_search_col (et); + if (et->is_grouped || will_be_grouped) { + et->need_rebuild = TRUE; + if (!et->rebuild_idle_id) { + g_object_run_dispose (G_OBJECT (et->group)); + et->group = NULL; + et->rebuild_idle_id = g_idle_add_full (20, changed_idle, et, NULL); + } + } + e_table_state_change (et); +} + +static void +sort_info_changed (ETableSortInfo *info, + ETable *et) +{ + clear_current_search_col (et); + e_table_state_change (et); +} + +static void +e_table_setup_header (ETable *e_table) +{ + gchar *pointer; + e_table->header_canvas = GNOME_CANVAS (e_canvas_new ()); + + gtk_widget_show (GTK_WIDGET (e_table->header_canvas)); + + pointer = g_strdup_printf ("%p", (gpointer) e_table); + + e_table->header_item = gnome_canvas_item_new ( + gnome_canvas_root (e_table->header_canvas), + e_table_header_item_get_type (), + "ETableHeader", e_table->header, + "full_header", e_table->full_header, + "sort_info", e_table->sort_info, + "dnd_code", pointer, + "table", e_table, + NULL); + + g_free (pointer); + + g_signal_connect ( + e_table->header_canvas, "size_allocate", + G_CALLBACK (header_canvas_size_allocate), e_table); + + g_object_set ( + e_table->header_canvas, "height-request", + E_TABLE_HEADER_ITEM (e_table->header_item)->height, NULL); +} + +static gboolean +table_canvas_reflow_idle (ETable *e_table) +{ + gdouble height, width; + gdouble oldheight, oldwidth; + GtkAllocation allocation; + + gtk_widget_get_allocation ( + GTK_WIDGET (e_table->table_canvas), &allocation); + + g_object_get ( + e_table->canvas_vbox, + "height", &height, "width", &width, NULL); + height = MAX ((gint) height, allocation.height); + width = MAX ((gint) width, allocation.width); + /* I have no idea why this needs to be -1, but it works. */ + gnome_canvas_get_scroll_region ( + GNOME_CANVAS (e_table->table_canvas), + NULL, NULL, &oldwidth, &oldheight); + + if (oldwidth != width - 1 || + oldheight != height - 1) { + gnome_canvas_set_scroll_region ( + GNOME_CANVAS (e_table->table_canvas), + 0, 0, width - 1, height - 1); + set_header_canvas_width (e_table); + } + e_table->reflow_idle_id = 0; + return FALSE; +} + +static void +table_canvas_size_allocate (GtkWidget *widget, + GtkAllocation *alloc, + ETable *e_table) +{ + gdouble width; + gdouble height; + GValue *val = g_new0 (GValue, 1); + g_value_init (val, G_TYPE_DOUBLE); + + width = alloc->width; + g_value_set_double (val, width); + g_object_get ( + e_table->canvas_vbox, + "height", &height, + NULL); + height = MAX ((gint) height, alloc->height); + + g_object_set ( + e_table->canvas_vbox, + "width", width, + NULL); + g_object_set_property (G_OBJECT (e_table->header), "width", val); + g_free (val); + if (e_table->reflow_idle_id) + g_source_remove (e_table->reflow_idle_id); + table_canvas_reflow_idle (e_table); + + e_table->size_allocated = TRUE; + + if (e_table->need_rebuild && !e_table->rebuild_idle_id) + e_table->rebuild_idle_id = g_idle_add_full (20, changed_idle, e_table, NULL); +} + +static void +table_canvas_reflow (GnomeCanvas *canvas, + ETable *e_table) +{ + if (!e_table->reflow_idle_id) + e_table->reflow_idle_id = g_idle_add_full ( + 400, (GSourceFunc) table_canvas_reflow_idle, + e_table, NULL); +} + +static void +click_to_add_cursor_change (ETableClickToAdd *etcta, + gint row, + gint col, + ETable *et) +{ + if (et->cursor_loc == E_TABLE_CURSOR_LOC_TABLE) { + e_selection_model_clear (E_SELECTION_MODEL (et->selection)); + } + et->cursor_loc = E_TABLE_CURSOR_LOC_ETCTA; +} + +static void +group_cursor_change (ETableGroup *etg, + gint row, + ETable *et) +{ + ETableCursorLoc old_cursor_loc; + + old_cursor_loc = et->cursor_loc; + + et->cursor_loc = E_TABLE_CURSOR_LOC_TABLE; + g_signal_emit (et, et_signals[CURSOR_CHANGE], 0, row); + + if (old_cursor_loc == E_TABLE_CURSOR_LOC_ETCTA && et->click_to_add) + e_table_click_to_add_commit (E_TABLE_CLICK_TO_ADD (et->click_to_add)); +} + +static void +group_cursor_activated (ETableGroup *etg, + gint row, + ETable *et) +{ + g_signal_emit (et, et_signals[CURSOR_ACTIVATED], 0, row); +} + +static void +group_double_click (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETable *et) +{ + g_signal_emit (et, et_signals[DOUBLE_CLICK], 0, row, col, event); +} + +static gboolean +group_right_click (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETable *et) +{ + gboolean return_val = FALSE; + + g_signal_emit ( + et, et_signals[RIGHT_CLICK], 0, + row, col, event, &return_val); + + return return_val; +} + +static gboolean +group_click (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETable *et) +{ + gboolean return_val = 0; + + g_signal_emit ( + et, et_signals[CLICK], 0, + row, col, event, &return_val); + + return return_val; +} + +static gboolean +group_key_press (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETable *et) +{ + gboolean return_val = FALSE; + GdkEventKey *key = (GdkEventKey *) event; + gint y, row_local, col_local; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + gdouble page_size; + gdouble upper; + gdouble value; + + scrollable = GTK_SCROLLABLE (et->table_canvas); + adjustment = gtk_scrollable_get_vadjustment (scrollable); + + switch (key->keyval) { + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + page_size = gtk_adjustment_get_page_size (adjustment); + upper = gtk_adjustment_get_value (adjustment); + value = gtk_adjustment_get_value (adjustment); + + y = CLAMP (value + (2 * page_size - 50), 0, upper); + y -= value; + e_table_get_cell_at (et, 30, y, &row_local, &col_local); + + if (row_local == -1) + row_local = e_table_model_row_count (et->model) - 1; + + row_local = e_table_view_to_model_row (et, row_local); + col_local = e_selection_model_cursor_col (E_SELECTION_MODEL (et->selection)); + e_selection_model_select_as_key_press ( + E_SELECTION_MODEL (et->selection), + row_local, col_local, key->state); + return_val = 1; + break; + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + page_size = gtk_adjustment_get_page_size (adjustment); + upper = gtk_adjustment_get_upper (adjustment); + value = gtk_adjustment_get_value (adjustment); + + y = CLAMP (value - (page_size - 50), 0, upper); + y -= value; + e_table_get_cell_at (et, 30, y, &row_local, &col_local); + + if (row_local == -1) + row_local = 0; + + row_local = e_table_view_to_model_row (et, row_local); + col_local = e_selection_model_cursor_col (E_SELECTION_MODEL (et->selection)); + e_selection_model_select_as_key_press ( + E_SELECTION_MODEL (et->selection), + row_local, col_local, key->state); + return_val = 1; + break; + case GDK_KEY_BackSpace: + init_search (et); + if (e_table_search_backspace (et->search)) + return TRUE; + /* Fall through */ + default: + init_search (et); + if ((key->state & ~(GDK_SHIFT_MASK | GDK_LOCK_MASK | + GDK_MOD1_MASK | GDK_MOD2_MASK | GDK_MOD3_MASK | + GDK_MOD4_MASK | GDK_MOD5_MASK)) == 0 + && ((key->keyval >= GDK_KEY_a && key->keyval <= GDK_KEY_z) || + (key->keyval >= GDK_KEY_A && key->keyval <= GDK_KEY_Z) || + (key->keyval >= GDK_KEY_0 && key->keyval <= GDK_KEY_9))) + e_table_search_input_character (et->search, key->keyval); + g_signal_emit ( + et, et_signals[KEY_PRESS], 0, + row, col, event, &return_val); + break; + } + return return_val; +} + +static gboolean +group_start_drag (ETableGroup *etg, + gint row, + gint col, + GdkEvent *event, + ETable *et) +{ + gboolean return_val = TRUE; + + g_signal_emit ( + et, et_signals[START_DRAG], 0, + row, col, event, &return_val); + + return return_val; +} + +static void +et_table_model_changed (ETableModel *model, + ETable *et) +{ + et->need_rebuild = TRUE; + if (!et->rebuild_idle_id) { + g_object_run_dispose (G_OBJECT (et->group)); + et->group = NULL; + et->rebuild_idle_id = g_idle_add_full (20, changed_idle, et, NULL); + } +} + +static void +et_table_row_changed (ETableModel *table_model, + gint row, + ETable *et) +{ + if (!et->need_rebuild) { + if (e_table_group_remove (et->group, row)) + e_table_group_add (et->group, row); + CHECK_HORIZONTAL (et); + } +} + +static void +et_table_cell_changed (ETableModel *table_model, + gint view_col, + gint row, + ETable *et) +{ + et_table_row_changed (table_model, row, et); +} + +static void +et_table_rows_inserted (ETableModel *table_model, + gint row, + gint count, + ETable *et) +{ + /* This number has already been decremented. */ + gint row_count = e_table_model_row_count (table_model); + if (!et->need_rebuild) { + gint i; + if (row != row_count - count) + e_table_group_increment (et->group, row, count); + for (i = 0; i < count; i++) + e_table_group_add (et->group, row + i); + CHECK_HORIZONTAL (et); + } +} + +static void +et_table_rows_deleted (ETableModel *table_model, + gint row, + gint count, + ETable *et) +{ + gint row_count = e_table_model_row_count (table_model); + if (!et->need_rebuild) { + gint i; + for (i = 0; i < count; i++) + e_table_group_remove (et->group, row + i); + if (row != row_count) + e_table_group_decrement (et->group, row, count); + CHECK_HORIZONTAL (et); + } +} + +static void +et_build_groups (ETable *et) +{ + gboolean was_grouped = et->is_grouped; + + et->is_grouped = e_table_sort_info_grouping_get_count (et->sort_info) > 0; + + et->group = e_table_group_new ( + GNOME_CANVAS_GROUP (et->canvas_vbox), + et->full_header, + et->header, + et->model, + et->sort_info, + 0); + + if (et->use_click_to_add_end) + e_canvas_vbox_add_item_start ( + E_CANVAS_VBOX (et->canvas_vbox), + GNOME_CANVAS_ITEM (et->group)); + else + e_canvas_vbox_add_item ( + E_CANVAS_VBOX (et->canvas_vbox), + GNOME_CANVAS_ITEM (et->group)); + + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (et->group), + "alternating_row_colors", et->alternating_row_colors, + "horizontal_draw_grid", et->horizontal_draw_grid, + "vertical_draw_grid", et->vertical_draw_grid, + "drawfocus", et->draw_focus, + "cursor_mode", et->cursor_mode, + "length_threshold", et->length_threshold, + "uniform_row_height", et->uniform_row_height, + "selection_model", et->selection, + NULL); + + g_signal_connect ( + et->group, "cursor_change", + G_CALLBACK (group_cursor_change), et); + g_signal_connect ( + et->group, "cursor_activated", + G_CALLBACK (group_cursor_activated), et); + g_signal_connect ( + et->group, "double_click", + G_CALLBACK (group_double_click), et); + g_signal_connect ( + et->group, "right_click", + G_CALLBACK (group_right_click), et); + g_signal_connect ( + et->group, "click", + G_CALLBACK (group_click), et); + g_signal_connect ( + et->group, "key_press", + G_CALLBACK (group_key_press), et); + g_signal_connect ( + et->group, "start_drag", + G_CALLBACK (group_start_drag), et); + + if (!(et->is_grouped) && was_grouped) + et_disconnect_model (et); + + if (et->is_grouped && (!was_grouped)) { + et->table_model_change_id = g_signal_connect ( + et->model, "model_changed", + G_CALLBACK (et_table_model_changed), et); + + et->table_row_change_id = g_signal_connect ( + et->model, "model_row_changed", + G_CALLBACK (et_table_row_changed), et); + + et->table_cell_change_id = g_signal_connect ( + et->model, "model_cell_changed", + G_CALLBACK (et_table_cell_changed), et); + + et->table_rows_inserted_id = g_signal_connect ( + et->model, "model_rows_inserted", + G_CALLBACK (et_table_rows_inserted), et); + + et->table_rows_deleted_id = g_signal_connect ( + et->model, "model_rows_deleted", + G_CALLBACK (et_table_rows_deleted), et); + + } + + if (et->is_grouped) + e_table_fill_table (et, et->model); +} + +static gboolean +changed_idle (gpointer data) +{ + ETable *et = E_TABLE (data); + + /* Wait until we have a valid size allocation. */ + if (et->need_rebuild && et->size_allocated) { + GtkWidget *widget; + GtkAllocation allocation; + + if (et->group) + g_object_run_dispose (G_OBJECT (et->group)); + et_build_groups (et); + + widget = GTK_WIDGET (et->table_canvas); + gtk_widget_get_allocation (widget, &allocation); + + g_object_set ( + et->canvas_vbox, + "width", (gdouble) allocation.width, + NULL); + + table_canvas_size_allocate (widget, &allocation, et); + + et->need_rebuild = 0; + } + + et->rebuild_idle_id = 0; + + CHECK_HORIZONTAL (et); + + return FALSE; +} + +static void +et_canvas_realize (GtkWidget *canvas, + ETable *e_table) +{ + GtkWidget *widget; + GtkStyle *style; + + widget = GTK_WIDGET (e_table->table_canvas); + style = gtk_widget_get_style (widget); + + gnome_canvas_item_set ( + e_table->white_item, + "fill_color_gdk", &style->base[GTK_STATE_NORMAL], + NULL); + + CHECK_HORIZONTAL (e_table); + set_header_width (e_table); +} + +static gboolean +white_item_event (GnomeCanvasItem *white_item, + GdkEvent *event, + ETable *e_table) +{ + gboolean return_val = 0; + + g_signal_emit ( + e_table, et_signals[WHITE_SPACE_EVENT], 0, + event, &return_val); + + return return_val; +} + +static void +et_eti_leave_edit (ETable *et) +{ + GnomeCanvas *canvas = et->table_canvas; + + if (gtk_widget_has_focus (GTK_WIDGET (canvas))) { + GnomeCanvasItem *item = GNOME_CANVAS (canvas)->focused_item; + + if (E_IS_TABLE_ITEM (item)) { + e_table_item_leave_edit_(E_TABLE_ITEM (item)); + } + } +} + +static gint +et_canvas_root_event (GnomeCanvasItem *root, + GdkEvent *event, + ETable *e_table) +{ + switch (event->type) { + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + if (event->button.button != 4 && event->button.button != 5) { + et_eti_leave_edit (e_table); + return TRUE; + } + break; + default: + break; + } + + return FALSE; +} + +/* Finds the first descendant of the group that is an ETableItem and focuses it */ +static void +focus_first_etable_item (ETableGroup *group) +{ + GnomeCanvasGroup *cgroup; + GList *l; + + cgroup = GNOME_CANVAS_GROUP (group); + + for (l = cgroup->item_list; l; l = l->next) { + GnomeCanvasItem *i; + + i = GNOME_CANVAS_ITEM (l->data); + + if (E_IS_TABLE_GROUP (i)) + focus_first_etable_item (E_TABLE_GROUP (i)); + else if (E_IS_TABLE_ITEM (i)) { + e_table_item_set_cursor (E_TABLE_ITEM (i), 0, 0); + gnome_canvas_item_grab_focus (i); + } + } +} + +/* Handler for focus events in the table_canvas; we have to repaint ourselves + * always, and also give the focus to some ETableItem if we get focused. + */ +static gint +table_canvas_focus_event_cb (GtkWidget *widget, + GdkEventFocus *event, + gpointer data) +{ + GnomeCanvas *canvas; + ECanvas *ecanvas; + ETable *etable; + + gtk_widget_queue_draw (widget); + canvas = GNOME_CANVAS (widget); + ecanvas = E_CANVAS (widget); + + if (!event->in) { + gtk_im_context_focus_out (ecanvas->im_context); + return FALSE; + } else { + gtk_im_context_focus_in (ecanvas->im_context); + } + + etable = E_TABLE (data); + + if (e_table_model_row_count (etable->model) < 1 + && (etable->click_to_add) + && !(E_TABLE_CLICK_TO_ADD (etable->click_to_add)->row)) { + gnome_canvas_item_grab_focus (etable->canvas_vbox); + gnome_canvas_item_grab_focus (etable->click_to_add); + } else if (!canvas->focused_item && etable->group) { + focus_first_etable_item (etable->group); + } else if (canvas->focused_item) { + ESelectionModel *selection = (ESelectionModel *) etable->selection; + + /* check whether click_to_add already got the focus */ + if (etable->click_to_add) { + GnomeCanvasItem *row = E_TABLE_CLICK_TO_ADD (etable->click_to_add)->row; + if (canvas->focused_item == row) + return TRUE; + } + + if (e_selection_model_cursor_row (selection) == -1) + focus_first_etable_item (etable->group); + } + + return FALSE; +} + +static gboolean +canvas_vbox_event (ECanvasVbox *vbox, + GdkEventKey *key, + ETable *etable) +{ + if (key->type != GDK_KEY_PRESS && + key->type != GDK_KEY_RELEASE) { + return FALSE; + } + switch (key->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + case GDK_KEY_ISO_Left_Tab: + if ((key->state & GDK_CONTROL_MASK) && etable->click_to_add) { + gnome_canvas_item_grab_focus (etable->click_to_add); + break; + } + default: + return FALSE; + } + + return TRUE; +} + +static gboolean +click_to_add_event (ETableClickToAdd *etcta, + GdkEventKey *key, + ETable *etable) +{ + if (key->type != GDK_KEY_PRESS && + key->type != GDK_KEY_RELEASE) { + return FALSE; + } + switch (key->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + case GDK_KEY_ISO_Left_Tab: + if (key->state & GDK_CONTROL_MASK) { + if (etable->group) { + if (e_table_model_row_count (etable->model) > 0) + focus_first_etable_item (etable->group); + else + gtk_widget_child_focus ( + gtk_widget_get_toplevel ( + GTK_WIDGET (etable->table_canvas)), + GTK_DIR_TAB_FORWARD); + break; + } + } + default: + return FALSE; + } + + return FALSE; +} + +static void +e_table_setup_table (ETable *e_table, + ETableHeader *full_header, + ETableHeader *header, + ETableModel *model) +{ + GtkWidget *widget; + GtkStyle *style; + + e_table->table_canvas = GNOME_CANVAS (e_canvas_new ()); + g_signal_connect ( + e_table->table_canvas, "size_allocate", + G_CALLBACK (table_canvas_size_allocate), e_table); + g_signal_connect ( + e_table->table_canvas, "focus_in_event", + G_CALLBACK (table_canvas_focus_event_cb), e_table); + g_signal_connect ( + e_table->table_canvas, "focus_out_event", + G_CALLBACK (table_canvas_focus_event_cb), e_table); + + g_signal_connect ( + e_table, "drag_begin", + G_CALLBACK (et_drag_begin), e_table); + g_signal_connect ( + e_table, "drag_end", + G_CALLBACK (et_drag_end), e_table); + g_signal_connect ( + e_table, "drag_data_get", + G_CALLBACK (et_drag_data_get), e_table); + g_signal_connect ( + e_table, "drag_data_delete", + G_CALLBACK (et_drag_data_delete), e_table); + g_signal_connect ( + e_table, "drag_motion", + G_CALLBACK (et_drag_motion), e_table); + g_signal_connect ( + e_table, "drag_leave", + G_CALLBACK (et_drag_leave), e_table); + g_signal_connect ( + e_table, "drag_drop", + G_CALLBACK (et_drag_drop), e_table); + g_signal_connect ( + e_table, "drag_data_received", + G_CALLBACK (et_drag_data_received), e_table); + + g_signal_connect ( + e_table->table_canvas, "reflow", + G_CALLBACK (table_canvas_reflow), e_table); + + widget = GTK_WIDGET (e_table->table_canvas); + style = gtk_widget_get_style (widget); + + gtk_widget_show (widget); + + e_table->white_item = gnome_canvas_item_new ( + gnome_canvas_root (e_table->table_canvas), + e_canvas_background_get_type (), + "fill_color_gdk", &style->base[GTK_STATE_NORMAL], + NULL); + + g_signal_connect ( + e_table->white_item, "event", + G_CALLBACK (white_item_event), e_table); + + g_signal_connect ( + e_table->table_canvas, "realize", + G_CALLBACK (et_canvas_realize), e_table); + + g_signal_connect ( + gnome_canvas_root (e_table->table_canvas), "event", + G_CALLBACK (et_canvas_root_event), e_table); + + e_table->canvas_vbox = gnome_canvas_item_new ( + gnome_canvas_root (e_table->table_canvas), + e_canvas_vbox_get_type (), + "spacing", 10.0, + NULL); + + g_signal_connect ( + e_table->canvas_vbox, "event", + G_CALLBACK (canvas_vbox_event), e_table); + + et_build_groups (e_table); + + if (e_table->use_click_to_add) { + e_table->click_to_add = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (e_table->canvas_vbox), + e_table_click_to_add_get_type (), + "header", e_table->header, + "model", e_table->model, + "message", e_table->click_to_add_message, + NULL); + + if (e_table->use_click_to_add_end) + e_canvas_vbox_add_item ( + E_CANVAS_VBOX (e_table->canvas_vbox), + e_table->click_to_add); + else + e_canvas_vbox_add_item_start ( + E_CANVAS_VBOX (e_table->canvas_vbox), + e_table->click_to_add); + + g_signal_connect ( + e_table->click_to_add, "event", + G_CALLBACK (click_to_add_event), e_table); + g_signal_connect ( + e_table->click_to_add, "cursor_change", + G_CALLBACK (click_to_add_cursor_change), e_table); + } +} + +static void +e_table_fill_table (ETable *e_table, + ETableModel *model) +{ + e_table_group_add_all (e_table->group); +} + +/** + * e_table_set_state_object: + * @e_table: The #ETable object to modify + * @state: The #ETableState to use + * + * This routine sets the state of the #ETable from the given + * #ETableState. + * + **/ +void +e_table_set_state_object (ETable *e_table, + ETableState *state) +{ + GValue *val; + GtkWidget *widget; + GtkAllocation allocation; + + val = g_new0 (GValue, 1); + g_value_init (val, G_TYPE_DOUBLE); + + connect_header (e_table, state); + + widget = GTK_WIDGET (e_table->table_canvas); + gtk_widget_get_allocation (widget, &allocation); + + g_value_set_double (val, (gdouble) allocation.width); + g_object_set_property (G_OBJECT (e_table->header), "width", val); + g_free (val); + + if (e_table->sort_info) { + if (e_table->group_info_change_id) + g_signal_handler_disconnect ( + e_table->sort_info, + e_table->group_info_change_id); + if (e_table->sort_info_change_id) + g_signal_handler_disconnect ( + e_table->sort_info, + e_table->sort_info_change_id); + g_object_unref (e_table->sort_info); + } + if (state->sort_info) { + e_table->sort_info = e_table_sort_info_duplicate (state->sort_info); + e_table_sort_info_set_can_group ( + e_table->sort_info, e_table->allow_grouping); + e_table->group_info_change_id = g_signal_connect ( + e_table->sort_info, "group_info_changed", + G_CALLBACK (group_info_changed), e_table); + + e_table->sort_info_change_id = g_signal_connect ( + e_table->sort_info, "sort_info_changed", + G_CALLBACK (sort_info_changed), e_table); + } + else + e_table->sort_info = NULL; + + if (e_table->sorter) + g_object_set ( + e_table->sorter, + "sort_info", e_table->sort_info, + NULL); + if (e_table->header_item) + g_object_set ( + e_table->header_item, + "ETableHeader", e_table->header, + "sort_info", e_table->sort_info, + NULL); + if (e_table->click_to_add) + g_object_set ( + e_table->click_to_add, + "header", e_table->header, + NULL); + + e_table->need_rebuild = TRUE; + if (!e_table->rebuild_idle_id) + e_table->rebuild_idle_id = g_idle_add_full (20, changed_idle, e_table, NULL); + + e_table_state_change (e_table); +} + +/** + * e_table_set_state: + * @e_table: The #ETable object to modify + * @state_str: a string representing an #ETableState + * + * This routine sets the state of the #ETable from a string. + * + **/ +void +e_table_set_state (ETable *e_table, + const gchar *state_str) +{ + ETableState *state; + + g_return_if_fail (E_IS_TABLE (e_table)); + g_return_if_fail (state_str != NULL); + + state = e_table_state_new (); + e_table_state_load_from_string (state, state_str); + + if (state->col_count > 0) + e_table_set_state_object (e_table, state); + + g_object_unref (state); +} + +/** + * e_table_load_state: + * @e_table: The #ETable object to modify + * @filename: name of the file to use + * + * This routine sets the state of the #ETable from a file. + * + **/ +void +e_table_load_state (ETable *e_table, + const gchar *filename) +{ + ETableState *state; + + g_return_if_fail (E_IS_TABLE (e_table)); + g_return_if_fail (filename != NULL); + + state = e_table_state_new (); + e_table_state_load_from_file (state, filename); + + if (state->col_count > 0) + e_table_set_state_object (e_table, state); + + g_object_unref (state); +} + +/** + * e_table_get_state_object: + * @e_table: #ETable object to act on + * + * Builds an #ETableState corresponding to the current state of the + * #ETable. + * + * Return value: + * The %ETableState object generated. + **/ +ETableState * +e_table_get_state_object (ETable *e_table) +{ + ETableState *state; + gint full_col_count; + gint i, j; + + state = e_table_state_new (); + if (state->sort_info) + g_object_unref (state->sort_info); + state->sort_info = e_table->sort_info; + g_object_ref (state->sort_info); + + state->col_count = e_table_header_count (e_table->header); + full_col_count = e_table_header_count (e_table->full_header); + state->columns = g_new (int, state->col_count); + state->expansions = g_new (double, state->col_count); + for (i = 0; i < state->col_count; i++) { + ETableCol *col = e_table_header_get_column (e_table->header, i); + state->columns[i] = -1; + for (j = 0; j < full_col_count; j++) { + if (col->col_idx == e_table_header_index (e_table->full_header, j)) { + state->columns[i] = j; + break; + } + } + state->expansions[i] = col->expansion; + } + + return state; +} + +/** + * e_table_get_state: + * @e_table: The #ETable to act on. + * + * Builds a state object based on the current state and returns the + * string corresponding to that state. + * + * Return value: + * A string describing the current state of the #ETable. + **/ +gchar *e_table_get_state (ETable *e_table) +{ + ETableState *state; + gchar *string; + + state = e_table_get_state_object (e_table); + string = e_table_state_save_to_string (state); + g_object_unref (state); + return string; +} + +/** + * e_table_save_state: + * @e_table: The #ETable to act on + * @filename: name of the file to save to + * + * Saves the state of the @e_table object into the file pointed by + * @filename. + * + **/ +void +e_table_save_state (ETable *e_table, + const gchar *filename) +{ + ETableState *state; + + state = e_table_get_state_object (e_table); + e_table_state_save_to_file (state, filename); + g_object_unref (state); +} + +static void +et_selection_model_selection_changed (ETableGroup *etg, + ETable *et) +{ + g_signal_emit (et, et_signals[SELECTION_CHANGE], 0); +} + +static void +et_selection_model_selection_row_changed (ETableGroup *etg, + gint row, + ETable *et) +{ + g_signal_emit (et, et_signals[SELECTION_CHANGE], 0); +} + +static ETable * +et_real_construct (ETable *e_table, + ETableModel *etm, + ETableExtras *ete, + ETableSpecification *specification, + ETableState *state) +{ + gint row = 0; + gint col_count, i; + GValue *val; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + + val = g_new0 (GValue, 1); + g_value_init (val, G_TYPE_OBJECT); + + if (ete) + g_object_ref (ete); + else { + ete = e_table_extras_new (); + } + + e_table->domain = g_strdup (specification->domain); + + e_table->use_click_to_add = specification->click_to_add; + e_table->use_click_to_add_end = specification->click_to_add_end; + e_table->click_to_add_message = specification->click_to_add_message ? + g_strdup ( + dgettext (e_table->domain, + specification->click_to_add_message)) : NULL; + e_table->alternating_row_colors = specification->alternating_row_colors; + e_table->horizontal_draw_grid = specification->horizontal_draw_grid; + e_table->vertical_draw_grid = specification->vertical_draw_grid; + e_table->draw_focus = specification->draw_focus; + e_table->cursor_mode = specification->cursor_mode; + e_table->full_header = e_table_spec_to_full_header (specification, ete); + + col_count = e_table_header_count (e_table->full_header); + for (i = 0; i < col_count; i++) { + ETableCol *col = e_table_header_get_column (e_table->full_header, i); + if (col && col->search) { + e_table->current_search_col = col; + e_table->search_col_set = TRUE; + break; + } + } + + e_table->model = etm; + g_object_ref (etm); + + connect_header (e_table, state); + e_table->horizontal_scrolling = specification->horizontal_scrolling; + e_table->horizontal_resize = specification->horizontal_resize; + e_table->allow_grouping = specification->allow_grouping; + + e_table->sort_info = g_object_ref (state->sort_info); + + e_table_sort_info_set_can_group ( + e_table->sort_info, e_table->allow_grouping); + + e_table->group_info_change_id = g_signal_connect ( + e_table->sort_info, "group_info_changed", + G_CALLBACK (group_info_changed), e_table); + + e_table->sort_info_change_id = g_signal_connect ( + e_table->sort_info, "sort_info_changed", + G_CALLBACK (sort_info_changed), e_table); + + g_value_set_object (val, e_table->sort_info); + g_object_set_property (G_OBJECT (e_table->header), "sort_info", val); + g_free (val); + + e_table->sorter = e_table_sorter_new ( + etm, e_table->full_header, e_table->sort_info); + + g_object_set ( + e_table->selection, + "model", etm, + "selection_mode", specification->selection_mode, + "cursor_mode", specification->cursor_mode, + "sorter", e_table->sorter, + "header", e_table->header, + NULL); + + g_signal_connect ( + e_table->selection, "selection_changed", + G_CALLBACK (et_selection_model_selection_changed), e_table); + g_signal_connect ( + e_table->selection, "selection_row_changed", + G_CALLBACK (et_selection_model_selection_row_changed), e_table); + + if (!specification->no_headers) + e_table_setup_header (e_table); + + e_table_setup_table ( + e_table, e_table->full_header, e_table->header, etm); + e_table_fill_table (e_table, etm); + + scrollable = GTK_SCROLLABLE (e_table->table_canvas); + + adjustment = gtk_scrollable_get_vadjustment (scrollable); + gtk_adjustment_set_step_increment (adjustment, 20); + + adjustment = gtk_scrollable_get_hadjustment (scrollable); + gtk_adjustment_set_step_increment (adjustment, 20); + + if (!specification->no_headers) { + /* The header */ + gtk_table_attach ( + GTK_TABLE (e_table), GTK_WIDGET (e_table->header_canvas), + 0, 1, 0 + row, 1 + row, + GTK_FILL | GTK_EXPAND, + GTK_FILL, 0, 0); + row++; + } + gtk_table_attach ( + GTK_TABLE (e_table), GTK_WIDGET (e_table->table_canvas), + 0, 1, 0 + row, 1 + row, + GTK_FILL | GTK_EXPAND, + GTK_FILL | GTK_EXPAND, + 0, 0); + + g_object_unref (ete); + + return e_table; +} + +/** + * e_table_construct: + * @e_table: The newly created #ETable object. + * @etm: The model for this table. + * @ete: An optional #ETableExtras. (%NULL is valid.) + * @spec_str: The spec. + * @state_str: An optional state. (%NULL is valid.) + * + * This is the internal implementation of e_table_new() for use by + * subclasses or language bindings. See e_table_new() for details. + * + * Return value: + * The passed in value @e_table or %NULL if there's an error. + **/ +ETable * +e_table_construct (ETable *e_table, + ETableModel *etm, + ETableExtras *ete, + const gchar *spec_str, + const gchar *state_str) +{ + ETableSpecification *specification; + ETableState *state; + + g_return_val_if_fail (E_IS_TABLE (e_table), NULL); + g_return_val_if_fail (E_IS_TABLE_MODEL (etm), NULL); + g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), NULL); + g_return_val_if_fail (spec_str != NULL, NULL); + + g_object_ref (etm); + + specification = e_table_specification_new (); + g_object_ref (specification); + if (!e_table_specification_load_from_string (specification, spec_str)) { + g_object_unref (specification); + return NULL; + } + + if (state_str) { + state = e_table_state_new (); + g_object_ref (state); + e_table_state_load_from_string (state, state_str); + if (state->col_count <= 0) { + g_object_unref (state); + state = specification->state; + g_object_ref (state); + } + } else { + state = specification->state; + g_object_ref (state); + } + + e_table = et_real_construct (e_table, etm, ete, specification, state); + + e_table->spec = specification; + g_object_unref (state); + + return e_table; +} + +/** + * e_table_construct_from_spec_file: + * @e_table: The newly created #ETable object. + * @etm: The model for this table. + * @ete: An optional #ETableExtras. (%NULL is valid.) + * @spec_fn: The filename of the spec. + * @state_fn: An optional state file. (%NULL is valid.) + * + * This is the internal implementation of e_table_new_from_spec_file() + * for use by subclasses or language bindings. See + * e_table_new_from_spec_file() for details. + * + * Return value: + * The passed in value @e_table or %NULL if there's an error. + **/ +ETable * +e_table_construct_from_spec_file (ETable *e_table, + ETableModel *etm, + ETableExtras *ete, + const gchar *spec_fn, + const gchar *state_fn) +{ + ETableSpecification *specification; + ETableState *state; + + g_return_val_if_fail (E_IS_TABLE (e_table), NULL); + g_return_val_if_fail (E_IS_TABLE_MODEL (etm), NULL); + g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), NULL); + g_return_val_if_fail (spec_fn != NULL, NULL); + + specification = e_table_specification_new (); + if (!e_table_specification_load_from_file (specification, spec_fn)) { + g_object_unref (specification); + return NULL; + } + + if (state_fn) { + state = e_table_state_new (); + if (!e_table_state_load_from_file (state, state_fn)) { + g_object_unref (state); + state = specification->state; + g_object_ref (state); + } + if (state->col_count <= 0) { + g_object_unref (state); + state = specification->state; + g_object_ref (state); + } + } else { + state = specification->state; + g_object_ref (state); + } + + e_table = et_real_construct (e_table, etm, ete, specification, state); + + e_table->spec = specification; + g_object_unref (state); + + return e_table; +} + +/** + * e_table_new: + * @etm: The model for this table. + * @ete: An optional #ETableExtras. (%NULL is valid.) + * @spec: The spec. + * @state: An optional state. (%NULL is valid.) + * + * This function creates an #ETable from the given parameters. The + * #ETableModel is a table model to be represented. The #ETableExtras + * is an optional set of pixbufs, cells, and sorting functions to be + * used when interpreting the spec. If you pass in %NULL it uses the + * default #ETableExtras. (See e_table_extras_new()). + * + * @spec is the specification of the set of viewable columns and the + * default sorting state and such. @state is an optional string + * specifying the current sorting state and such. If @state is NULL, + * then the default state from the spec will be used. + * + * Return value: + * The newly created #ETable or %NULL if there's an error. + **/ +GtkWidget * +e_table_new (ETableModel *etm, + ETableExtras *ete, + const gchar *spec, + const gchar *state) +{ + ETable *e_table; + + g_return_val_if_fail (E_IS_TABLE_MODEL (etm), NULL); + g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), NULL); + g_return_val_if_fail (spec != NULL, NULL); + + e_table = g_object_new (E_TYPE_TABLE, NULL); + + e_table = e_table_construct (e_table, etm, ete, spec, state); + + return GTK_WIDGET (e_table); +} + +/** + * e_table_new_from_spec_file: + * @etm: The model for this table. + * @ete: An optional #ETableExtras. (%NULL is valid.) + * @spec_fn: The filename of the spec. + * @state_fn: An optional state file. (%NULL is valid.) + * + * This is very similar to e_table_new(), except instead of passing in + * strings you pass in the file names of the spec and state to load. + * + * @spec_fn is the filename of the spec to load. If this file doesn't + * exist, e_table_new_from_spec_file will return %NULL. + * + * @state_fn is the filename of the initial state to load. If this is + * %NULL or if the specified file doesn't exist, the default state + * from the spec file is used. + * + * Return value: + * The newly created #ETable or %NULL if there's an error. + **/ +GtkWidget * +e_table_new_from_spec_file (ETableModel *etm, + ETableExtras *ete, + const gchar *spec_fn, + const gchar *state_fn) +{ + ETable *e_table; + + g_return_val_if_fail (E_IS_TABLE_MODEL (etm), NULL); + g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), NULL); + g_return_val_if_fail (spec_fn != NULL, NULL); + + e_table = g_object_new (E_TYPE_TABLE, NULL); + + e_table = e_table_construct_from_spec_file (e_table, etm, ete, spec_fn, state_fn); + + return GTK_WIDGET (e_table); +} + +/** + * e_table_set_cursor_row: + * @e_table: The #ETable to set the cursor row of + * @row: The row number + * + * Sets the cursor row and the selection to the given row number. + **/ +void +e_table_set_cursor_row (ETable *e_table, + gint row) +{ + g_return_if_fail (E_IS_TABLE (e_table)); + g_return_if_fail (row >= 0); + + g_object_set ( + e_table->selection, + "cursor_row", row, + NULL); +} + +/** + * e_table_get_cursor_row: + * @e_table: The #ETable to query + * + * Calculates the cursor row. -1 means that we don't have a cursor. + * + * Return value: + * Cursor row + **/ +gint +e_table_get_cursor_row (ETable *e_table) +{ + gint row; + g_return_val_if_fail (E_IS_TABLE (e_table), -1); + + g_object_get ( + e_table->selection, + "cursor_row", &row, + NULL); + return row; +} + +/** + * e_table_selected_row_foreach: + * @e_table: The #ETable to act on + * @callback: The callback function to call + * @closure: The value passed to the callback's closure argument + * + * Calls the given @callback function once for every selected row. + * + * If you change the selection or delete or add rows to the table + * during these callbacks, problems can occur. A standard thing to do + * is to create a list of rows or objects the function is called upon + * and then act upon that list. (In inverse order if it's rows.) + **/ +void +e_table_selected_row_foreach (ETable *e_table, + EForeachFunc callback, + gpointer closure) +{ + g_return_if_fail (E_IS_TABLE (e_table)); + + e_selection_model_foreach (E_SELECTION_MODEL (e_table->selection), + callback, + closure); +} + +/** + * e_table_selected_count: + * @e_table: The #ETable to query + * + * Counts the number of selected rows. + * + * Return value: + * The number of rows selected. + **/ +gint +e_table_selected_count (ETable *e_table) +{ + g_return_val_if_fail (E_IS_TABLE (e_table), -1); + + return e_selection_model_selected_count (E_SELECTION_MODEL (e_table->selection)); +} + +/** + * e_table_select_all: + * @table: The #ETable to modify + * + * Selects all the rows in @table. + **/ +void +e_table_select_all (ETable *table) +{ + g_return_if_fail (E_IS_TABLE (table)); + + e_selection_model_select_all (E_SELECTION_MODEL (table->selection)); +} + +/** + * e_table_invert_selection: + * @table: The #ETable to modify + * + * Inverts the selection in @table. + **/ +void +e_table_invert_selection (ETable *table) +{ + g_return_if_fail (E_IS_TABLE (table)); + + e_selection_model_invert_selection (E_SELECTION_MODEL (table->selection)); +} + +/** + * e_table_get_printable: + * @e_table: #ETable to query + * + * Used for printing your #ETable. + * + * Return value: + * The #EPrintable to print. + **/ +EPrintable * +e_table_get_printable (ETable *e_table) +{ + g_return_val_if_fail (E_IS_TABLE (e_table), NULL); + + return e_table_group_get_printable (e_table->group); +} + +/** + * e_table_right_click_up: + * @table: The #ETable to modify. + * + * Call this function when you're done handling the right click if you + * return TRUE from the "right_click" signal. + **/ +void +e_table_right_click_up (ETable *table) +{ + e_selection_model_right_click_up (E_SELECTION_MODEL (table->selection)); +} + +/** + * e_table_commit_click_to_add: + * @table: The #ETable to modify + * + * Commits the current values in the click to add to the table. + **/ +void +e_table_commit_click_to_add (ETable *table) +{ + et_eti_leave_edit (table); + if (table->click_to_add) + e_table_click_to_add_commit ( + E_TABLE_CLICK_TO_ADD (table->click_to_add)); +} + +static void +et_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETable *etable = E_TABLE (object); + + switch (property_id) { + case PROP_MODEL: + g_value_set_object (value, etable->model); + break; + case PROP_UNIFORM_ROW_HEIGHT: + g_value_set_boolean (value, etable->uniform_row_height); + break; + case PROP_ALWAYS_SEARCH: + g_value_set_boolean (value, etable->always_search); + break; + case PROP_USE_CLICK_TO_ADD: + g_value_set_boolean (value, etable->use_click_to_add); + break; + case PROP_HADJUSTMENT: + if (etable->table_canvas) + g_object_get_property ( + G_OBJECT (etable->table_canvas), + "hadjustment", value); + else + g_value_set_object (value, NULL); + break; + case PROP_VADJUSTMENT: + if (etable->table_canvas) + g_object_get_property ( + G_OBJECT (etable->table_canvas), + "vadjustment", value); + else + g_value_set_object (value, NULL); + break; + case PROP_HSCROLL_POLICY: + if (etable->table_canvas) + g_object_get_property ( + G_OBJECT (etable->table_canvas), + "hscroll-policy", value); + else + g_value_set_enum (value, 0); + break; + case PROP_VSCROLL_POLICY: + if (etable->table_canvas) + g_object_get_property ( + G_OBJECT (etable->table_canvas), + "vscroll-policy", value); + else + g_value_set_enum (value, 0); + break; + default: + break; + } +} + +typedef struct { + gchar *arg; + gboolean setting; +} bool_closure; + +static void +et_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ETable *etable = E_TABLE (object); + + switch (property_id) { + case PROP_LENGTH_THRESHOLD: + etable->length_threshold = g_value_get_int (value); + if (etable->group) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etable->group), + "length_threshold", + etable->length_threshold, + NULL); + } + break; + case PROP_UNIFORM_ROW_HEIGHT: + etable->uniform_row_height = g_value_get_boolean (value); + if (etable->group) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etable->group), + "uniform_row_height", + etable->uniform_row_height, + NULL); + } + break; + case PROP_ALWAYS_SEARCH: + if (etable->always_search == g_value_get_boolean (value)) + return; + + etable->always_search = g_value_get_boolean (value); + clear_current_search_col (etable); + break; + case PROP_USE_CLICK_TO_ADD: + if (etable->use_click_to_add == g_value_get_boolean (value)) + return; + + etable->use_click_to_add = g_value_get_boolean (value); + clear_current_search_col (etable); + + if (etable->use_click_to_add) { + etable->click_to_add = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (etable->canvas_vbox), + e_table_click_to_add_get_type (), + "header", etable->header, + "model", etable->model, + "message", etable->click_to_add_message, + NULL); + + if (etable->use_click_to_add_end) + e_canvas_vbox_add_item ( + E_CANVAS_VBOX (etable->canvas_vbox), + etable->click_to_add); + else + e_canvas_vbox_add_item_start ( + E_CANVAS_VBOX (etable->canvas_vbox), + etable->click_to_add); + + g_signal_connect ( + etable->click_to_add, "cursor_change", + G_CALLBACK (click_to_add_cursor_change), + etable); + } else { + g_object_run_dispose (G_OBJECT (etable->click_to_add)); + etable->click_to_add = NULL; + } + break; + case PROP_HADJUSTMENT: + if (etable->table_canvas) + g_object_set_property ( + G_OBJECT (etable->table_canvas), + "hadjustment", value); + break; + case PROP_VADJUSTMENT: + if (etable->table_canvas) + g_object_set_property ( + G_OBJECT (etable->table_canvas), + "vadjustment", value); + break; + case PROP_HSCROLL_POLICY: + if (etable->table_canvas) + g_object_set_property ( + G_OBJECT (etable->table_canvas), + "hscroll-policy", value); + break; + case PROP_VSCROLL_POLICY: + if (etable->table_canvas) + g_object_set_property ( + G_OBJECT (etable->table_canvas), + "vscroll-policy", value); + break; + } +} + +/** + * e_table_get_next_row: + * @e_table: The #ETable to query + * @model_row: The model row to go from + * + * This function is used when your table is sorted, but you're using + * model row numbers. It returns the next row in sorted order as a model row. + * + * Return value: + * The model row number. + **/ +gint +e_table_get_next_row (ETable *e_table, + gint model_row) +{ + g_return_val_if_fail (E_IS_TABLE (e_table), -1); + + if (e_table->sorter) { + gint i; + i = e_sorter_model_to_sorted (E_SORTER (e_table->sorter), model_row); + i++; + if (i < e_table_model_row_count (e_table->model)) { + return e_sorter_sorted_to_model (E_SORTER (e_table->sorter), i); + } else + return -1; + } else + if (model_row < e_table_model_row_count (e_table->model) - 1) + return model_row + 1; + else + return -1; +} + +/** + * e_table_get_prev_row: + * @e_table: The #ETable to query + * @model_row: The model row to go from + * + * This function is used when your table is sorted, but you're using + * model row numbers. It returns the previous row in sorted order as + * a model row. + * + * Return value: + * The model row number. + **/ +gint +e_table_get_prev_row (ETable *e_table, + gint model_row) +{ + g_return_val_if_fail (E_IS_TABLE (e_table), -1); + + if (e_table->sorter) { + gint i; + i = e_sorter_model_to_sorted (E_SORTER (e_table->sorter), model_row); + i--; + if (i >= 0) + return e_sorter_sorted_to_model (E_SORTER (e_table->sorter), i); + else + return -1; + } else + return model_row - 1; +} + +/** + * e_table_model_to_view_row: + * @e_table: The #ETable to query + * @model_row: The model row number + * + * Turns a model row into a view row. + * + * Return value: + * The view row number. + **/ +gint +e_table_model_to_view_row (ETable *e_table, + gint model_row) +{ + g_return_val_if_fail (E_IS_TABLE (e_table), -1); + + if (e_table->sorter) + return e_sorter_model_to_sorted (E_SORTER (e_table->sorter), model_row); + else + return model_row; +} + +/** + * e_table_view_to_model_row: + * @e_table: The #ETable to query + * @view_row: The view row number + * + * Turns a view row into a model row. + * + * Return value: + * The model row number. + **/ +gint +e_table_view_to_model_row (ETable *e_table, + gint view_row) +{ + g_return_val_if_fail (E_IS_TABLE (e_table), -1); + + if (e_table->sorter) + return e_sorter_sorted_to_model (E_SORTER (e_table->sorter), view_row); + else + return view_row; +} + +/** + * e_table_get_cell_at: + * @table: An #ETable widget + * @x: X coordinate for the pixel + * @y: Y coordinate for the pixel + * @row_return: Pointer to return the row value + * @col_return: Pointer to return the column value + * + * Return the row and column for the cell in which the pixel at (@x, @y) is + * contained. + **/ +void +e_table_get_cell_at (ETable *table, + gint x, + gint y, + gint *row_return, + gint *col_return) +{ + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + + g_return_if_fail (E_IS_TABLE (table)); + g_return_if_fail (row_return != NULL); + g_return_if_fail (col_return != NULL); + + /* FIXME it would be nice if it could handle a NULL row_return or + * col_return gracefully. */ + + scrollable = GTK_SCROLLABLE (table->table_canvas); + + adjustment = gtk_scrollable_get_hadjustment (scrollable); + x += gtk_adjustment_get_value (adjustment); + + adjustment = gtk_scrollable_get_vadjustment (scrollable); + y += gtk_adjustment_get_value (adjustment); + + e_table_group_compute_location ( + table->group, &x, &y, row_return, col_return); +} + +/** + * e_table_get_cell_geometry: + * @table: The #ETable. + * @row: The row to get the geometry of. + * @col: The col to get the geometry of. + * @x_return: Returns the x coordinate of the upper left hand corner + * of the cell with respect to the widget. + * @y_return: Returns the y coordinate of the upper left hand corner + * of the cell with respect to the widget. + * @width_return: Returns the width of the cell. + * @height_return: Returns the height of the cell. + * + * Returns the x, y, width, and height of the given cell. These can + * all be #NULL and they just won't be set. + **/ +void +e_table_get_cell_geometry (ETable *table, + gint row, + gint col, + gint *x_return, + gint *y_return, + gint *width_return, + gint *height_return) +{ + GtkAllocation allocation; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + + g_return_if_fail (E_IS_TABLE (table)); + + scrollable = GTK_SCROLLABLE (table->table_canvas); + + e_table_group_get_cell_geometry ( + table->group, &row, &col, x_return, y_return, + width_return, height_return); + + if (x_return && table->table_canvas) { + adjustment = gtk_scrollable_get_hadjustment (scrollable); + (*x_return) -= gtk_adjustment_get_value (adjustment); + } + + if (y_return) { + if (table->table_canvas) { + adjustment = gtk_scrollable_get_vadjustment (scrollable); + (*y_return) -= gtk_adjustment_get_value (adjustment); + } + + if (table->header_canvas) { + gtk_widget_get_allocation ( + GTK_WIDGET (table->header_canvas), + &allocation); + (*y_return) += allocation.height; + } + } +} + +/** + * e_table_get_mouse_over_cell: + * + * Similar to e_table_get_cell_at, only here we check + * based on the mouse motion information in the group. + **/ +void +e_table_get_mouse_over_cell (ETable *table, + gint *row, + gint *col) +{ + g_return_if_fail (E_IS_TABLE (table)); + + if (!table->group) + return; + + e_table_group_get_mouse_over (table->group, row, col); +} + +/** + * e_table_get_selection_model: + * @table: The #ETable to query + * + * Returns the table's #ESelectionModel in case you want to access it + * directly. + * + * Return value: + * The #ESelectionModel. + **/ +ESelectionModel * +e_table_get_selection_model (ETable *table) +{ + g_return_val_if_fail (E_IS_TABLE (table), NULL); + + return E_SELECTION_MODEL (table->selection); +} + +struct _ETableDragSourceSite +{ + GdkModifierType start_button_mask; + GtkTargetList *target_list; /* Targets for drag data */ + GdkDragAction actions; /* Possible actions */ + GdkPixbuf *pixbuf; /* Icon for drag data */ + + /* Stored button press information to detect drag beginning */ + gint state; + gint x, y; + gint row, col; +}; + +typedef enum +{ + GTK_DRAG_STATUS_DRAG, + GTK_DRAG_STATUS_WAIT, + GTK_DRAG_STATUS_DROP +} GtkDragStatus; + +typedef struct _GtkDragDestInfo GtkDragDestInfo; +typedef struct _GtkDragSourceInfo GtkDragSourceInfo; + +struct _GtkDragDestInfo +{ + GtkWidget *widget; /* Widget in which drag is in */ + GdkDragContext *context; /* Drag context */ + GtkDragSourceInfo *proxy_source; /* Set if this is a proxy drag */ + GtkSelectionData *proxy_data; /* Set while retrieving proxied data */ + guint dropped : 1; /* Set after we receive a drop */ + guint32 proxy_drop_time; /* Timestamp for proxied drop */ + guint proxy_drop_wait : 1; /* Set if we are waiting for a + * status reply before sending + * a proxied drop on. + */ + gint drop_x, drop_y; /* Position of drop */ +}; + +struct _GtkDragSourceInfo +{ + GtkWidget *widget; + GtkTargetList *target_list; /* Targets for drag data */ + GdkDragAction possible_actions; /* Actions allowed by source */ + GdkDragContext *context; /* drag context */ + GtkWidget *icon_window; /* Window for drag */ + GtkWidget *ipc_widget; /* GtkInvisible for grab, message passing */ + GdkCursor *cursor; /* Cursor for drag */ + gint hot_x, hot_y; /* Hot spot for drag */ + gint button; /* mouse button starting drag */ + + GtkDragStatus status; /* drag status */ + GdkEvent *last_event; /* motion event waiting for response */ + + gint start_x, start_y; /* Initial position */ + gint cur_x, cur_y; /* Current Position */ + + GList *selections; /* selections we've claimed */ + + GtkDragDestInfo *proxy_dest; /* Set if this is a proxy drag */ + + guint drop_timeout; /* Timeout for aborting drop */ + guint destroy_icon : 1; /* If true, destroy icon_window + */ +}; + +/* Drag & drop stuff. */ +/* Target */ + +/** + * e_table_drag_get_data: + * @table: + * @row: + * @col: + * @context: + * @target: + * @time: + * + * + **/ +void +e_table_drag_get_data (ETable *table, + gint row, + gint col, + GdkDragContext *context, + GdkAtom target, + guint32 time) +{ + g_return_if_fail (E_IS_TABLE (table)); + + gtk_drag_get_data ( + GTK_WIDGET (table), + context, + target, + time); +} + +/** + * e_table_drag_highlight: + * @table: The #ETable to highlight + * @row: The row number of the cell to highlight + * @col: The column number of the cell to highlight + * + * Set col to -1 to highlight the entire row. If row is -1, this is + * identical to e_table_drag_unhighlight(). + **/ +void +e_table_drag_highlight (ETable *table, + gint row, + gint col) +{ + GtkAllocation allocation; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + GtkStyle *style; + + g_return_if_fail (E_IS_TABLE (table)); + + scrollable = GTK_SCROLLABLE (table->table_canvas); + style = gtk_widget_get_style (GTK_WIDGET (table)); + gtk_widget_get_allocation (GTK_WIDGET (scrollable), &allocation); + + if (row != -1) { + gint x, y, width, height; + if (col == -1) { + e_table_get_cell_geometry (table, row, 0, &x, &y, &width, &height); + x = 0; + width = allocation.width; + } else { + e_table_get_cell_geometry (table, row, col, &x, &y, &width, &height); + adjustment = gtk_scrollable_get_hadjustment (scrollable); + x += gtk_adjustment_get_value (adjustment); + } + + adjustment = gtk_scrollable_get_vadjustment (scrollable); + y += gtk_adjustment_get_value (adjustment); + + if (table->drop_highlight == NULL) { + table->drop_highlight = gnome_canvas_item_new ( + gnome_canvas_root (table->table_canvas), + gnome_canvas_rect_get_type (), + "fill_color", NULL, + "outline_color_gdk", &style->fg[GTK_STATE_NORMAL], + NULL); + } + gnome_canvas_item_set ( + table->drop_highlight, + "x1", (gdouble) x, + "x2", (gdouble) x + width - 1, + "y1", (gdouble) y, + "y2", (gdouble) y + height - 1, + NULL); + } else { + if (table->drop_highlight) { + g_object_run_dispose (G_OBJECT (table->drop_highlight)); + table->drop_highlight = NULL; + } + } +} + +/** + * e_table_drag_unhighlight: + * @table: The #ETable to unhighlight + * + * Removes the highlight from an #ETable. + **/ +void +e_table_drag_unhighlight (ETable *table) +{ + g_return_if_fail (E_IS_TABLE (table)); + + if (table->drop_highlight) { + g_object_run_dispose (G_OBJECT (table->drop_highlight)); + table->drop_highlight = NULL; + } +} + +void +e_table_drag_dest_set (ETable *table, + GtkDestDefaults flags, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions) +{ + g_return_if_fail (E_IS_TABLE (table)); + + gtk_drag_dest_set ( + GTK_WIDGET (table), flags, targets, n_targets, actions); +} + +void +e_table_drag_dest_set_proxy (ETable *table, + GdkWindow *proxy_window, + GdkDragProtocol protocol, + gboolean use_coordinates) +{ + g_return_if_fail (E_IS_TABLE (table)); + + gtk_drag_dest_set_proxy ( + GTK_WIDGET (table), proxy_window, protocol, use_coordinates); +} + +/* + * There probably should be functions for setting the targets + * as a GtkTargetList + */ + +void +e_table_drag_dest_unset (GtkWidget *widget) +{ + g_return_if_fail (E_IS_TABLE (widget)); + + gtk_drag_dest_unset (widget); +} + +/* Source side */ + +static gint +et_real_start_drag (ETable *table, + gint row, + gint col, + GdkEvent *event) +{ + GtkDragSourceInfo *info; + GdkDragContext *context; + ETableDragSourceSite *site; + + if (table->do_drag) { + site = table->site; + + site->state = 0; + context = e_table_drag_begin ( + table, row, col, + site->target_list, + site->actions, + 1, event); + + if (context) { + info = g_dataset_get_data (context, "gtk-info"); + + if (info && !info->icon_window) { + if (site->pixbuf) + gtk_drag_set_icon_pixbuf ( + context, + site->pixbuf, + -2, -2); + else + gtk_drag_set_icon_default (context); + } + } + return TRUE; + } + return FALSE; +} + +/** + * e_table_drag_source_set: + * @table: The #ETable to set up as a drag site + * @start_button_mask: Mask of allowed buttons to start drag + * @targets: Table of targets for this source + * @n_targets: Number of targets in @targets + * @actions: Actions allowed for this source + * + * Registers this table as a drag site, and possibly adds default behaviors. + **/ +void +e_table_drag_source_set (ETable *table, + GdkModifierType start_button_mask, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions) +{ + ETableDragSourceSite *site; + GtkWidget *canvas; + + g_return_if_fail (E_IS_TABLE (table)); + + canvas = GTK_WIDGET (table->table_canvas); + site = table->site; + + gtk_widget_add_events ( + canvas, + gtk_widget_get_events (canvas) | + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_BUTTON_MOTION_MASK | GDK_STRUCTURE_MASK); + + table->do_drag = TRUE; + + if (site) { + if (site->target_list) + gtk_target_list_unref (site->target_list); + } else { + site = g_new0 (ETableDragSourceSite, 1); + table->site = site; + } + + site->start_button_mask = start_button_mask; + + if (targets) + site->target_list = gtk_target_list_new (targets, n_targets); + else + site->target_list = NULL; + + site->actions = actions; +} + +/** + * e_table_drag_source_unset: + * @table: The #ETable to un set up as a drag site + * + * Unregisters this #ETable as a drag site. + **/ +void +e_table_drag_source_unset (ETable *table) +{ + ETableDragSourceSite *site; + + g_return_if_fail (E_IS_TABLE (table)); + + site = table->site; + + if (site) { + if (site->target_list) + gtk_target_list_unref (site->target_list); + g_free (site); + table->site = NULL; + } + table->do_drag = FALSE; +} + +/* There probably should be functions for setting the targets + * as a GtkTargetList + */ + +/** + * e_table_drag_begin: + * @table: The #ETable to drag from + * @row: The row number of the cell + * @col: The col number of the cell + * @targets: The list of targets supported by the drag + * @actions: The available actions supported by the drag + * @button: The button held down for the drag + * @event: The event that initiated the drag + * + * Start a drag from this cell. + * + * Return value: + * The drag context. + **/ +GdkDragContext * +e_table_drag_begin (ETable *table, + gint row, + gint col, + GtkTargetList *targets, + GdkDragAction actions, + gint button, + GdkEvent *event) +{ + g_return_val_if_fail (E_IS_TABLE (table), NULL); + + table->drag_row = row; + table->drag_col = col; + + return gtk_drag_begin ( + GTK_WIDGET (table), targets, actions, button, event); +} + +static void +et_drag_begin (GtkWidget *widget, + GdkDragContext *context, + ETable *et) +{ + g_signal_emit ( + et, et_signals[TABLE_DRAG_BEGIN], 0, + et->drag_row, et->drag_col, context); +} + +static void +et_drag_end (GtkWidget *widget, + GdkDragContext *context, + ETable *et) +{ + g_signal_emit ( + et, et_signals[TABLE_DRAG_END], 0, + et->drag_row, et->drag_col, context); +} + +static void +et_drag_data_get (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time, + ETable *et) +{ + g_signal_emit ( + et, et_signals[TABLE_DRAG_DATA_GET], 0, + et->drag_row, et->drag_col, context, selection_data, + info, time); +} + +static void +et_drag_data_delete (GtkWidget *widget, + GdkDragContext *context, + ETable *et) +{ + g_signal_emit ( + et, et_signals[TABLE_DRAG_DATA_DELETE], 0, + et->drag_row, et->drag_col, context); +} + +static gboolean +do_drag_motion (ETable *et, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + gboolean ret_val; + gint row = -1, col = -1; + + e_table_get_cell_at (et, x, y, &row, &col); + + if (row != et->drop_row && col != et->drop_row) { + g_signal_emit ( + et, et_signals[TABLE_DRAG_LEAVE], 0, + et->drop_row, et->drop_col, context, time); + } + + et->drop_row = row; + et->drop_col = col; + + g_signal_emit ( + et, et_signals[TABLE_DRAG_MOTION], 0, + et->drop_row, et->drop_col, context, x, y, time, &ret_val); + + return ret_val; +} + +static gboolean +scroll_timeout (gpointer data) +{ + ETable *et = data; + gint dx = 0, dy = 0; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + gdouble old_h_value; + gdouble new_h_value; + gdouble old_v_value; + gdouble new_v_value; + gdouble page_size; + gdouble lower; + gdouble upper; + + if (et->scroll_direction & ET_SCROLL_DOWN) + dy += 20; + if (et->scroll_direction & ET_SCROLL_UP) + dy -= 20; + + if (et->scroll_direction & ET_SCROLL_RIGHT) + dx += 20; + if (et->scroll_direction & ET_SCROLL_LEFT) + dx -= 20; + + scrollable = GTK_SCROLLABLE (et->table_canvas); + + adjustment = gtk_scrollable_get_hadjustment (scrollable); + + lower = gtk_adjustment_get_lower (adjustment); + upper = gtk_adjustment_get_upper (adjustment); + page_size = gtk_adjustment_get_page_size (adjustment); + + old_h_value = gtk_adjustment_get_value (adjustment); + new_h_value = CLAMP (old_h_value + dx, lower, upper - page_size); + + gtk_adjustment_set_value (adjustment, new_h_value); + + adjustment = gtk_scrollable_get_vadjustment (scrollable); + + lower = gtk_adjustment_get_lower (adjustment); + upper = gtk_adjustment_get_upper (adjustment); + page_size = gtk_adjustment_get_page_size (adjustment); + + old_v_value = gtk_adjustment_get_value (adjustment); + new_v_value = CLAMP (old_v_value + dy, lower, upper - page_size); + + gtk_adjustment_set_value (adjustment, new_v_value); + + if (new_h_value != old_h_value || new_v_value != old_v_value) + do_drag_motion ( + et, + et->last_drop_context, + et->last_drop_x, + et->last_drop_y, + et->last_drop_time); + + return TRUE; +} + +static void +scroll_on (ETable *et, + guint scroll_direction) +{ + if (et->scroll_idle_id == 0 || scroll_direction != et->scroll_direction) { + if (et->scroll_idle_id != 0) + g_source_remove (et->scroll_idle_id); + et->scroll_direction = scroll_direction; + et->scroll_idle_id = g_timeout_add (100, scroll_timeout, et); + } +} + +static void +scroll_off (ETable *et) +{ + if (et->scroll_idle_id) { + g_source_remove (et->scroll_idle_id); + et->scroll_idle_id = 0; + } +} + +static void +context_destroyed (gpointer data) +{ + ETable *et = data; + /* if (!G_OBJECT_DESTROYED (et)) */ +/* FIXME: */ + { + et->last_drop_x = 0; + et->last_drop_y = 0; + et->last_drop_time = 0; + et->last_drop_context = NULL; + scroll_off (et); + } + g_object_unref (et); +} + +static void +context_connect (ETable *et, + GdkDragContext *context) +{ + if (g_dataset_get_data (context, "e-table") == NULL) { + g_object_ref (et); + g_dataset_set_data_full (context, "e-table", et, context_destroyed); + } +} + +static void +et_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time, + ETable *et) +{ + g_signal_emit ( + et, et_signals[TABLE_DRAG_LEAVE], 0, + et->drop_row, et->drop_col, context, time); + + et->drop_row = -1; + et->drop_col = -1; + + scroll_off (et); +} + +static gboolean +et_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETable *et) +{ + GtkAllocation allocation; + gboolean ret_val; + guint direction = 0; + + gtk_widget_get_allocation (widget, &allocation); + + et->last_drop_x = x; + et->last_drop_y = y; + et->last_drop_time = time; + et->last_drop_context = context; + context_connect (et, context); + + ret_val = do_drag_motion (et, context, x, y, time); + + if (y < 20) + direction |= ET_SCROLL_UP; + if (y > allocation.height - 20) + direction |= ET_SCROLL_DOWN; + if (x < 20) + direction |= ET_SCROLL_LEFT; + if (x > allocation.width - 20) + direction |= ET_SCROLL_RIGHT; + + if (direction != 0) + scroll_on (et, direction); + else + scroll_off (et); + + return ret_val; +} + +static gboolean +et_drag_drop (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETable *et) +{ + gboolean ret_val; + gint row, col; + + e_table_get_cell_at (et, x, y, &row, &col); + + if (row != et->drop_row && col != et->drop_row) { + g_signal_emit ( + et, et_signals[TABLE_DRAG_LEAVE], 0, + et->drop_row, et->drop_col, context, time); + g_signal_emit ( + et, et_signals[TABLE_DRAG_MOTION], 0, + row, col, context, x, y, time, &ret_val); + } + et->drop_row = row; + et->drop_col = col; + g_signal_emit ( + et, et_signals[TABLE_DRAG_DROP], 0, + et->drop_row, et->drop_col, context, x, y, time, &ret_val); + et->drop_row = -1; + et->drop_col = -1; + + scroll_off (et); + + return ret_val; +} + +static void +et_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time, + ETable *et) +{ + gint row, col; + + e_table_get_cell_at (et, x, y, &row, &col); + + g_signal_emit ( + et, et_signals[TABLE_DRAG_DATA_RECEIVED], 0, + row, col, context, x, y, selection_data, info, time); +} + +static void +e_table_class_init (ETableClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + object_class = (GObjectClass *) class; + widget_class = (GtkWidgetClass *) class; + + object_class->dispose = et_dispose; + object_class->finalize = et_finalize; + object_class->set_property = et_set_property; + object_class->get_property = et_get_property; + + widget_class->grab_focus = et_grab_focus; + widget_class->unrealize = et_unrealize; + widget_class->get_preferred_width = et_get_preferred_width; + widget_class->get_preferred_height = et_get_preferred_height; + + widget_class->focus = et_focus; + + class->cursor_change = NULL; + class->cursor_activated = NULL; + class->selection_change = NULL; + class->double_click = NULL; + class->right_click = NULL; + class->click = NULL; + class->key_press = NULL; + class->start_drag = et_real_start_drag; + class->state_change = NULL; + class->white_space_event = NULL; + + class->table_drag_begin = NULL; + class->table_drag_end = NULL; + class->table_drag_data_get = NULL; + class->table_drag_data_delete = NULL; + + class->table_drag_leave = NULL; + class->table_drag_motion = NULL; + class->table_drag_drop = NULL; + class->table_drag_data_received = NULL; + + et_signals[CURSOR_CHANGE] = g_signal_new ( + "cursor_change", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, cursor_change), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, G_TYPE_INT); + + et_signals[CURSOR_ACTIVATED] = g_signal_new ( + "cursor_activated", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, cursor_activated), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, G_TYPE_INT); + + et_signals[SELECTION_CHANGE] = g_signal_new ( + "selection_change", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, selection_change), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + et_signals[DOUBLE_CLICK] = g_signal_new ( + "double_click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, double_click), + NULL, NULL, + e_marshal_NONE__INT_INT_BOXED, + G_TYPE_NONE, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[RIGHT_CLICK] = g_signal_new ( + "right_click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, right_click), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[CLICK] = g_signal_new ( + "click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, click), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[KEY_PRESS] = g_signal_new ( + "key_press", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, key_press), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[START_DRAG] = g_signal_new ( + "start_drag", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, start_drag), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_INT_BOXED, + G_TYPE_BOOLEAN, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[STATE_CHANGE] = g_signal_new ( + "state_change", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, state_change), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + et_signals[WHITE_SPACE_EVENT] = g_signal_new ( + "white_space_event", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, white_space_event), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__BOXED, + G_TYPE_BOOLEAN, 1, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[TABLE_DRAG_BEGIN] = g_signal_new ( + "table_drag_begin", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, table_drag_begin), + NULL, NULL, + e_marshal_NONE__INT_INT_OBJECT, + G_TYPE_NONE, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT); + + et_signals[TABLE_DRAG_END] = g_signal_new ( + "table_drag_end", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, table_drag_end), + NULL, NULL, + e_marshal_NONE__INT_INT_OBJECT, + G_TYPE_NONE, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT); + + et_signals[TABLE_DRAG_DATA_GET] = g_signal_new ( + "table_drag_data_get", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, table_drag_data_get), + NULL, NULL, + e_marshal_NONE__INT_INT_OBJECT_BOXED_UINT_UINT, + G_TYPE_NONE, 6, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + GTK_TYPE_SELECTION_DATA | G_SIGNAL_TYPE_STATIC_SCOPE, + G_TYPE_UINT, + G_TYPE_UINT); + + et_signals[TABLE_DRAG_DATA_DELETE] = g_signal_new ( + "table_drag_data_delete", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, table_drag_data_delete), + NULL, NULL, + e_marshal_NONE__INT_INT_OBJECT, + G_TYPE_NONE, 3, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT); + + et_signals[TABLE_DRAG_LEAVE] = g_signal_new ( + "table_drag_leave", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, table_drag_leave), + NULL, NULL, + e_marshal_NONE__INT_INT_OBJECT_UINT, + G_TYPE_NONE, 4, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + G_TYPE_UINT); + + et_signals[TABLE_DRAG_MOTION] = g_signal_new ( + "table_drag_motion", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, table_drag_motion), + NULL, NULL, + e_marshal_BOOLEAN__INT_INT_OBJECT_INT_INT_UINT, + G_TYPE_BOOLEAN, 6, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + G_TYPE_INT, + G_TYPE_INT, + G_TYPE_UINT); + + et_signals[TABLE_DRAG_DROP] = g_signal_new ( + "table_drag_drop", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, table_drag_drop), + NULL, NULL, + e_marshal_BOOLEAN__INT_INT_OBJECT_INT_INT_UINT, + G_TYPE_BOOLEAN, 6, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + G_TYPE_INT, + G_TYPE_INT, + G_TYPE_UINT); + + et_signals[TABLE_DRAG_DATA_RECEIVED] = g_signal_new ( + "table_drag_data_received", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETableClass, table_drag_data_received), + NULL, NULL, + e_marshal_NONE__INT_INT_OBJECT_INT_INT_BOXED_UINT_UINT, + G_TYPE_NONE, 8, + G_TYPE_INT, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + G_TYPE_INT, + G_TYPE_INT, + GTK_TYPE_SELECTION_DATA | G_SIGNAL_TYPE_STATIC_SCOPE, + G_TYPE_UINT, + G_TYPE_UINT); + + g_object_class_install_property ( + object_class, + PROP_LENGTH_THRESHOLD, + g_param_spec_int ( + "length_threshold", + "Length Threshold", + NULL, + 0, G_MAXINT, 0, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_UNIFORM_ROW_HEIGHT, + g_param_spec_boolean ( + "uniform_row_height", + "Uniform row height", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_ALWAYS_SEARCH, + g_param_spec_boolean ( + "always_search", + "Always search", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_USE_CLICK_TO_ADD, + g_param_spec_boolean ( + "use_click_to_add", + "Use click to add", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MODEL, + g_param_spec_object ( + "model", + "Model", + NULL, + E_TYPE_TABLE_MODEL, + G_PARAM_READABLE)); + + gtk_widget_class_install_style_property ( + widget_class, + g_param_spec_int ( + "vertical-spacing", + "Vertical Row Spacing", + "Vertical space between rows. " + "It is added to top and to bottom of a row", + 0, G_MAXINT, 3, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /* Scrollable interface */ + g_object_class_override_property ( + object_class, PROP_HADJUSTMENT, "hadjustment"); + g_object_class_override_property ( + object_class, PROP_VADJUSTMENT, "vadjustment"); + g_object_class_override_property ( + object_class, PROP_HSCROLL_POLICY, "hscroll-policy"); + g_object_class_override_property ( + object_class, PROP_VSCROLL_POLICY, "vscroll-policy"); + + gal_a11y_e_table_init (); +} + +void +e_table_freeze_state_change (ETable *table) +{ + g_return_if_fail (table != NULL); + + table->state_change_freeze++; + if (table->state_change_freeze == 1) + table->state_changed = FALSE; + + g_return_if_fail (table->state_change_freeze != 0); +} + +void +e_table_thaw_state_change (ETable *table) +{ + g_return_if_fail (table != NULL); + g_return_if_fail (table->state_change_freeze != 0); + + table->state_change_freeze--; + if (table->state_change_freeze == 0 && table->state_changed) { + table->state_changed = FALSE; + e_table_state_change (table); + } +} diff --git a/e-util/e-table.h b/e-util/e-table.h new file mode 100644 index 0000000000..8370e440df --- /dev/null +++ b/e-util/e-table.h @@ -0,0 +1,403 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Miguel de Icaza <miguel@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TABLE_H_ +#define _E_TABLE_H_ + +#include <libgnomecanvas/libgnomecanvas.h> +#include <gtk/gtk.h> +#include <libxml/tree.h> + +#include <e-util/e-printable.h> +#include <e-util/e-table-extras.h> +#include <e-util/e-table-group.h> +#include <e-util/e-table-header.h> +#include <e-util/e-table-item.h> +#include <e-util/e-table-model.h> +#include <e-util/e-table-search.h> +#include <e-util/e-table-selection-model.h> +#include <e-util/e-table-sort-info.h> +#include <e-util/e-table-sorter.h> +#include <e-util/e-table-specification.h> +#include <e-util/e-table-state.h> + +/* Standard GObject macros */ +#define E_TYPE_TABLE \ + (e_table_get_type ()) +#define E_TABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TABLE, ETable)) +#define E_TABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TABLE, ETableClass)) +#define E_IS_TABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TABLE)) +#define E_IS_TABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TABLE)) +#define E_TABLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TABLE, ETableClass)) + +G_BEGIN_DECLS + +typedef struct _ETable ETable; +typedef struct _ETableClass ETableClass; + +typedef struct _ETableDragSourceSite ETableDragSourceSite; + +typedef enum { + E_TABLE_CURSOR_LOC_NONE = 0, + E_TABLE_CURSOR_LOC_ETCTA = 1 << 0, + E_TABLE_CURSOR_LOC_TABLE = 1 << 1 +} ETableCursorLoc; + +struct _ETable { + GtkTable parent; + + ETableModel *model; + + ETableHeader *full_header, *header; + + GnomeCanvasItem *canvas_vbox; + ETableGroup *group; + + ETableSortInfo *sort_info; + ETableSorter *sorter; + + ETableSelectionModel *selection; + ETableCursorLoc cursor_loc; + ETableSpecification *spec; + + ETableSearch *search; + + ETableCol *current_search_col; + + guint search_search_id; + guint search_accept_id; + + gint table_model_change_id; + gint table_row_change_id; + gint table_cell_change_id; + gint table_rows_inserted_id; + gint table_rows_deleted_id; + + gint group_info_change_id; + gint sort_info_change_id; + + gint structure_change_id; + gint expansion_change_id; + gint dimension_change_id; + + gint reflow_idle_id; + gint scroll_idle_id; + + GnomeCanvas *header_canvas, *table_canvas; + + GnomeCanvasItem *header_item, *root; + + GnomeCanvasItem *white_item; + + gint length_threshold; + + gint rebuild_idle_id; + guint need_rebuild : 1; + guint size_allocated : 1; + + /* + * Configuration settings + */ + guint alternating_row_colors : 1; + guint horizontal_draw_grid : 1; + guint vertical_draw_grid : 1; + guint draw_focus : 1; + guint row_selection_active : 1; + + guint horizontal_scrolling : 1; + guint horizontal_resize : 1; + + guint is_grouped : 1; + + guint scroll_direction : 4; + + guint do_drag : 1; + + guint uniform_row_height : 1; + guint allow_grouping : 1; + + guint always_search : 1; + guint search_col_set : 1; + + gchar *click_to_add_message; + GnomeCanvasItem *click_to_add; + gboolean use_click_to_add; + gboolean use_click_to_add_end; + + ECursorMode cursor_mode; + + gint drop_row; + gint drop_col; + GnomeCanvasItem *drop_highlight; + gint last_drop_x; + gint last_drop_y; + gint last_drop_time; + GdkDragContext *last_drop_context; + + gint drag_row; + gint drag_col; + ETableDragSourceSite *site; + + gint header_width; + + gchar *domain; + + gboolean state_changed; + guint state_change_freeze; +}; + +struct _ETableClass { + GtkTableClass parent_class; + + void (*cursor_change) (ETable *et, + gint row); + void (*cursor_activated) (ETable *et, + gint row); + void (*selection_change) (ETable *et); + void (*double_click) (ETable *et, + gint row, + gint col, + GdkEvent *event); + gboolean (*right_click) (ETable *et, + gint row, + gint col, + GdkEvent *event); + gboolean (*click) (ETable *et, + gint row, + gint col, + GdkEvent *event); + gboolean (*key_press) (ETable *et, + gint row, + gint col, + GdkEvent *event); + gboolean (*start_drag) (ETable *et, + gint row, + gint col, + GdkEvent *event); + void (*state_change) (ETable *et); + gboolean (*white_space_event) (ETable *et, + GdkEvent *event); + + /* Source side drag signals */ + void (*table_drag_begin) (ETable *table, + gint row, + gint col, + GdkDragContext *context); + void (*table_drag_end) (ETable *table, + gint row, + gint col, + GdkDragContext *context); + void (*table_drag_data_get) (ETable *table, + gint row, + gint col, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time); + void (*table_drag_data_delete) + (ETable *table, + gint row, + gint col, + GdkDragContext *context); + + /* Target side drag signals */ + void (*table_drag_leave) (ETable *table, + gint row, + gint col, + GdkDragContext *context, + guint time); + gboolean (*table_drag_motion) (ETable *table, + gint row, + gint col, + GdkDragContext *context, + gint x, + gint y, + guint time); + gboolean (*table_drag_drop) (ETable *table, + gint row, + gint col, + GdkDragContext *context, + gint x, + gint y, + guint time); + void (*table_drag_data_received) + (ETable *table, + gint row, + gint col, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time); +}; + +GType e_table_get_type (void) G_GNUC_CONST; +ETable * e_table_construct (ETable *e_table, + ETableModel *etm, + ETableExtras *ete, + const gchar *spec, + const gchar *state); +GtkWidget * e_table_new (ETableModel *etm, + ETableExtras *ete, + const gchar *spec, + const gchar *state); + +/* Create an ETable using files. */ +ETable * e_table_construct_from_spec_file + (ETable *e_table, + ETableModel *etm, + ETableExtras *ete, + const gchar *spec_fn, + const gchar *state_fn); +GtkWidget * e_table_new_from_spec_file (ETableModel *etm, + ETableExtras *ete, + const gchar *spec_fn, + const gchar *state_fn); + +/* To save the state */ +gchar * e_table_get_state (ETable *e_table); +void e_table_save_state (ETable *e_table, + const gchar *filename); +ETableState *e_table_get_state_object (ETable *e_table); + +/* note that it is more efficient to provide the state at creation time */ +void e_table_set_state (ETable *e_table, + const gchar *state); +void e_table_set_state_object (ETable *e_table, + ETableState *state); +void e_table_load_state (ETable *e_table, + const gchar *filename); +void e_table_set_cursor_row (ETable *e_table, + gint row); + +/* -1 means we don't have the cursor. This is in model rows. */ +gint e_table_get_cursor_row (ETable *e_table); +void e_table_selected_row_foreach (ETable *e_table, + EForeachFunc callback, + gpointer closure); +gint e_table_selected_count (ETable *e_table); +EPrintable * e_table_get_printable (ETable *e_table); +gint e_table_get_next_row (ETable *e_table, + gint model_row); +gint e_table_get_prev_row (ETable *e_table, + gint model_row); +gint e_table_model_to_view_row (ETable *e_table, + gint model_row); +gint e_table_view_to_model_row (ETable *e_table, + gint view_row); +void e_table_get_cell_at (ETable *table, + gint x, + gint y, + gint *row_return, + gint *col_return); +void e_table_get_mouse_over_cell (ETable *table, + gint *row, + gint *col); +void e_table_get_cell_geometry (ETable *table, + gint row, + gint col, + gint *x_return, + gint *y_return, + gint *width_return, + gint *height_return); + +/* Useful accessor functions. */ +ESelectionModel *e_table_get_selection_model (ETable *table); + +/* Drag & drop stuff. */ +/* Target */ +void e_table_drag_get_data (ETable *table, + gint row, + gint col, + GdkDragContext *context, + GdkAtom target, + guint32 time); +void e_table_drag_highlight (ETable *table, + gint row, + gint col); /* col == -1 to highlight entire row. */ +void e_table_drag_unhighlight (ETable *table); +void e_table_drag_dest_set (ETable *table, + GtkDestDefaults flags, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions); +void e_table_drag_dest_set_proxy (ETable *table, + GdkWindow *proxy_window, + GdkDragProtocol protocol, + gboolean use_coordinates); + +/* There probably should be functions for setting the targets + * as a GtkTargetList + */ +void e_table_drag_dest_unset (GtkWidget *widget); + +/* Source side */ +void e_table_drag_source_set (ETable *table, + GdkModifierType start_button_mask, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions); +void e_table_drag_source_unset (ETable *table); + +/* There probably should be functions for setting the targets + * as a GtkTargetList + */ +GdkDragContext *e_table_drag_begin (ETable *table, + gint row, + gint col, + GtkTargetList *targets, + GdkDragAction actions, + gint button, + GdkEvent *event); + +/* selection stuff */ +void e_table_select_all (ETable *table); +void e_table_invert_selection (ETable *table); + +/* This function is only needed in single_selection_mode. */ +void e_table_right_click_up (ETable *table); + +void e_table_commit_click_to_add (ETable *table); + +void e_table_freeze_state_change (ETable *table); +void e_table_thaw_state_change (ETable *table); + +G_END_DECLS + +#endif /* _E_TABLE_H_ */ + diff --git a/e-util/e-text-event-processor-emacs-like.c b/e-util/e-text-event-processor-emacs-like.c index 2a42ae939c..c734cf84d4 100644 --- a/e-util/e-text-event-processor-emacs-like.c +++ b/e-util/e-text-event-processor-emacs-like.c @@ -29,7 +29,6 @@ #include <gdk/gdkkeysyms.h> #include "e-text-event-processor-emacs-like.h" -#include "e-util.h" static gint e_text_event_processor_emacs_like_event (ETextEventProcessor *tep, diff --git a/e-util/e-text-event-processor-emacs-like.h b/e-util/e-text-event-processor-emacs-like.h index 0b9c6c143c..5a8890d519 100644 --- a/e-util/e-text-event-processor-emacs-like.h +++ b/e-util/e-text-event-processor-emacs-like.h @@ -21,6 +21,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef __E_TEXT_EVENT_PROCESSOR_EMACS_LIKE_H__ #define __E_TEXT_EVENT_PROCESSOR_EMACS_LIKE_H__ diff --git a/e-util/e-text-event-processor-types.h b/e-util/e-text-event-processor-types.h index d7d0bb3854..cf7da4f5aa 100644 --- a/e-util/e-text-event-processor-types.h +++ b/e-util/e-text-event-processor-types.h @@ -21,6 +21,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef __E_TEXT_EVENT_PROCESSOR_TYPES_H__ #define __E_TEXT_EVENT_PROCESSOR_TYPES_H__ diff --git a/e-util/e-text-event-processor.c b/e-util/e-text-event-processor.c index a5da7810dd..7988bd6973 100644 --- a/e-util/e-text-event-processor.c +++ b/e-util/e-text-event-processor.c @@ -27,7 +27,6 @@ #include <glib/gi18n.h> #include "e-text-event-processor.h" -#include "e-util.h" static void e_text_event_processor_set_property (GObject *object, guint property_id, diff --git a/e-util/e-text-event-processor.h b/e-util/e-text-event-processor.h index cf14ebb286..203e2de236 100644 --- a/e-util/e-text-event-processor.h +++ b/e-util/e-text-event-processor.h @@ -20,6 +20,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef __E_TEXT_EVENT_PROCESSOR_H__ #define __E_TEXT_EVENT_PROCESSOR_H__ diff --git a/e-util/e-text-model-repos.c b/e-util/e-text-model-repos.c new file mode 100644 index 0000000000..b56a213215 --- /dev/null +++ b/e-util/e-text-model-repos.c @@ -0,0 +1,74 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Jon Trowbridge <trow@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-text-model-repos.h" + +#define MODEL_CLAMP(model, pos) (CLAMP((pos), 0, strlen((model)->text))) + +gint +e_repos_absolute (gint pos, + gpointer data) +{ + EReposAbsolute *info = (EReposAbsolute *) data; + g_return_val_if_fail (data, -1); + + pos = info->pos; + if (pos < 0) { + gint len = e_text_model_get_text_length (info->model); + pos += len + 1; + } + + return e_text_model_validate_position (info->model, pos); +} + +gint +e_repos_insert_shift (gint pos, + gpointer data) +{ + EReposInsertShift *info = (EReposInsertShift *) data; + g_return_val_if_fail (data, -1); + + if (pos >= info->pos) + pos += info->len; + + return e_text_model_validate_position (info->model, pos); +} + +gint +e_repos_delete_shift (gint pos, + gpointer data) +{ + EReposDeleteShift *info = (EReposDeleteShift *) data; + g_return_val_if_fail (data, -1); + + if (pos > info->pos + info->len) + pos -= info->len; + else if (pos > info->pos) + pos = info->pos; + + return e_text_model_validate_position (info->model, pos); +} diff --git a/e-util/e-text-model-repos.h b/e-util/e-text-model-repos.h new file mode 100644 index 0000000000..1450c02715 --- /dev/null +++ b/e-util/e-text-model-repos.h @@ -0,0 +1,58 @@ +/* + * e-text-model-repos.h - Standard ETextModelReposFn definitions + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Jon Trowbridge <trow@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_TEXT_MODEL_REPOS_H +#define E_TEXT_MODEL_REPOS_H + +#include "e-text-model.h" + +typedef struct { + ETextModel *model; + gint pos; /* Position to move to. Negative values count from the end buffer. + (i.e. -1 puts cursor at the end, -2 one character from end, etc.) */ +} EReposAbsolute; + +gint e_repos_absolute (gint pos, gpointer data); + +typedef struct { + ETextModel *model; + gint pos; /* Location of first inserted character. */ + gint len; /* Number of characters inserted. */ +} EReposInsertShift; + +gint e_repos_insert_shift (gint pos, gpointer data); + +typedef struct { + ETextModel *model; + gint pos; /* Location of first deleted character. */ + gint len; /* Number of characters deleted. */ +} EReposDeleteShift; + +gint e_repos_delete_shift (gint pos, gpointer data); + +#endif diff --git a/e-util/e-text-model.c b/e-util/e-text-model.c new file mode 100644 index 0000000000..ab6bff8ff3 --- /dev/null +++ b/e-util/e-text-model.c @@ -0,0 +1,642 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#undef PARANOID_DEBUGGING + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-text-model.h" + +#include <ctype.h> +#include <string.h> + +#include <gtk/gtk.h> + +#include "e-marshal.h" +#include "e-text-model-repos.h" + +#define E_TEXT_MODEL_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TEXT_MODEL, ETextModelPrivate)) + +enum { + E_TEXT_MODEL_CHANGED, + E_TEXT_MODEL_REPOSITION, + E_TEXT_MODEL_OBJECT_ACTIVATED, + E_TEXT_MODEL_CANCEL_COMPLETION, + E_TEXT_MODEL_LAST_SIGNAL +}; + +static guint signals[E_TEXT_MODEL_LAST_SIGNAL] = { 0 }; + +struct _ETextModelPrivate { + GString *text; +}; + +static gint e_text_model_real_validate_position + (ETextModel *, gint pos); +static const gchar * + e_text_model_real_get_text (ETextModel *model); +static gint e_text_model_real_get_text_length + (ETextModel *model); +static void e_text_model_real_set_text (ETextModel *model, + const gchar *text); +static void e_text_model_real_insert (ETextModel *model, + gint postion, + const gchar *text); +static void e_text_model_real_insert_length (ETextModel *model, + gint postion, + const gchar *text, + gint length); +static void e_text_model_real_delete (ETextModel *model, + gint postion, + gint length); + +G_DEFINE_TYPE (ETextModel, e_text_model, G_TYPE_OBJECT) + +static void +e_text_model_finalize (GObject *object) +{ + ETextModelPrivate *priv; + + priv = E_TEXT_MODEL_GET_PRIVATE (object); + + g_string_free (priv->text, TRUE); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_text_model_parent_class)->finalize (object); +} + +static void +e_text_model_class_init (ETextModelClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ETextModelPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = e_text_model_finalize; + + signals[E_TEXT_MODEL_CHANGED] = g_signal_new ( + "changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETextModelClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[E_TEXT_MODEL_REPOSITION] = g_signal_new ( + "reposition", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETextModelClass, reposition), + NULL, NULL, + e_marshal_NONE__POINTER_POINTER, + G_TYPE_NONE, 2, + G_TYPE_POINTER, + G_TYPE_POINTER); + + signals[E_TEXT_MODEL_OBJECT_ACTIVATED] = g_signal_new ( + "object_activated", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETextModelClass, object_activated), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + signals[E_TEXT_MODEL_CANCEL_COMPLETION] = g_signal_new ( + "cancel_completion", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETextModelClass, cancel_completion), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /* No default signal handlers. */ + class->changed = NULL; + class->reposition = NULL; + class->object_activated = NULL; + + class->validate_pos = e_text_model_real_validate_position; + + class->get_text = e_text_model_real_get_text; + class->get_text_len = e_text_model_real_get_text_length; + class->set_text = e_text_model_real_set_text; + class->insert = e_text_model_real_insert; + class->insert_length = e_text_model_real_insert_length; + class->delete = e_text_model_real_delete; + + /* We explicitly don't define default handlers for these. */ + class->objectify = NULL; + class->obj_count = NULL; + class->get_nth_obj = NULL; +} + +static void +e_text_model_init (ETextModel *model) +{ + model->priv = E_TEXT_MODEL_GET_PRIVATE (model); + model->priv->text = g_string_new (""); +} + +static gint +e_text_model_real_validate_position (ETextModel *model, + gint pos) +{ + gint len = e_text_model_get_text_length (model); + + if (pos < 0) + pos = 0; + else if (pos > len) + pos = len; + + return pos; +} + +static const gchar * +e_text_model_real_get_text (ETextModel *model) +{ + if (model->priv->text) + return model->priv->text->str; + else + return ""; +} + +static gint +e_text_model_real_get_text_length (ETextModel *model) +{ + return g_utf8_strlen (model->priv->text->str, -1); +} + +static void +e_text_model_real_set_text (ETextModel *model, + const gchar *text) +{ + EReposAbsolute repos; + gboolean changed = FALSE; + + if (text == NULL) { + changed = (*model->priv->text->str != '\0'); + + g_string_set_size (model->priv->text, 0); + + } else if (*model->priv->text->str == '\0' || + strcmp (model->priv->text->str, text)) { + + g_string_assign (model->priv->text, text); + + changed = TRUE; + } + + if (changed) { + e_text_model_changed (model); + repos.model = model; + repos.pos = -1; + e_text_model_reposition (model, e_repos_absolute, &repos); + } +} + +static void +e_text_model_real_insert (ETextModel *model, + gint position, + const gchar *text) +{ + e_text_model_insert_length (model, position, text, strlen (text)); +} + +static void +e_text_model_real_insert_length (ETextModel *model, + gint position, + const gchar *text, + gint length) +{ + EReposInsertShift repos; + gint model_len = e_text_model_real_get_text_length (model); + gchar *offs; + const gchar *p; + gint byte_length, l; + + if (position > model_len) + return; + + offs = g_utf8_offset_to_pointer (model->priv->text->str, position); + + for (p = text, l = 0; + l < length; + p = g_utf8_next_char (p), l++); + + byte_length = p - text; + + g_string_insert_len ( + model->priv->text, + offs - model->priv->text->str, + text, byte_length); + + e_text_model_changed (model); + + repos.model = model; + repos.pos = position; + repos.len = length; + + e_text_model_reposition (model, e_repos_insert_shift, &repos); +} + +static void +e_text_model_real_delete (ETextModel *model, + gint position, + gint length) +{ + EReposDeleteShift repos; + gint byte_position, byte_length; + gchar *offs, *p; + gint l; + + offs = g_utf8_offset_to_pointer (model->priv->text->str, position); + byte_position = offs - model->priv->text->str; + + for (p = offs, l = 0; + l < length; + p = g_utf8_next_char (p), l++); + + byte_length = p - offs; + + g_string_erase ( + model->priv->text, + byte_position, byte_length); + + e_text_model_changed (model); + + repos.model = model; + repos.pos = position; + repos.len = length; + + e_text_model_reposition (model, e_repos_delete_shift, &repos); +} + +void +e_text_model_changed (ETextModel *model) +{ + ETextModelClass *class; + + g_return_if_fail (E_IS_TEXT_MODEL (model)); + + class = E_TEXT_MODEL_GET_CLASS (model); + + /* + Objectify before emitting any signal. + While this method could, in theory, do pretty much anything, it is meant + for scanning objects and converting substrings into embedded objects. + */ + if (class->objectify != NULL) + class->objectify (model); + + g_signal_emit (model, signals[E_TEXT_MODEL_CHANGED], 0); +} + +void +e_text_model_cancel_completion (ETextModel *model) +{ + g_return_if_fail (E_IS_TEXT_MODEL (model)); + + g_signal_emit (model, signals[E_TEXT_MODEL_CANCEL_COMPLETION], 0); +} + +void +e_text_model_reposition (ETextModel *model, + ETextModelReposFn fn, + gpointer repos_data) +{ + g_return_if_fail (E_IS_TEXT_MODEL (model)); + g_return_if_fail (fn != NULL); + + g_signal_emit ( + model, signals[E_TEXT_MODEL_REPOSITION], 0, fn, repos_data); +} + +gint +e_text_model_validate_position (ETextModel *model, + gint pos) +{ + ETextModelClass *class; + + g_return_val_if_fail (E_IS_TEXT_MODEL (model), 0); + + class = E_TEXT_MODEL_GET_CLASS (model); + + if (class->validate_pos != NULL) + pos = class->validate_pos (model, pos); + + return pos; +} + +const gchar * +e_text_model_get_text (ETextModel *model) +{ + ETextModelClass *class; + + g_return_val_if_fail (E_IS_TEXT_MODEL (model), NULL); + + class = E_TEXT_MODEL_GET_CLASS (model); + + if (class->get_text == NULL) + return ""; + + return class->get_text (model); +} + +gint +e_text_model_get_text_length (ETextModel *model) +{ + ETextModelClass *class; + + g_return_val_if_fail (E_IS_TEXT_MODEL (model), 0); + + class = E_TEXT_MODEL_GET_CLASS (model); + + if (class->get_text_len (model)) { + + gint len = class->get_text_len (model); + +#ifdef PARANOID_DEBUGGING + const gchar *str = e_text_model_get_text (model); + gint len2 = str ? g_utf8_strlen (str, -1) : 0; + if (len != len) + g_error ("\"%s\" length reported as %d, not %d.", str, len, len2); +#endif + + return len; + + } else { + /* Calculate length the old-fashioned way... */ + const gchar *str = e_text_model_get_text (model); + return str ? g_utf8_strlen (str, -1) : 0; + } +} + +void +e_text_model_set_text (ETextModel *model, + const gchar *text) +{ + ETextModelClass *class; + + g_return_if_fail (E_IS_TEXT_MODEL (model)); + + class = E_TEXT_MODEL_GET_CLASS (model); + + if (class->set_text != NULL) + class->set_text (model, text); +} + +void +e_text_model_insert (ETextModel *model, + gint position, + const gchar *text) +{ + ETextModelClass *class; + + g_return_if_fail (E_IS_TEXT_MODEL (model)); + + if (text == NULL) + return; + + class = E_TEXT_MODEL_GET_CLASS (model); + + if (class->insert != NULL) + class->insert (model, position, text); +} + +void +e_text_model_insert_length (ETextModel *model, + gint position, + const gchar *text, + gint length) +{ + ETextModelClass *class; + + g_return_if_fail (E_IS_TEXT_MODEL (model)); + g_return_if_fail (length >= 0); + + if (text == NULL || length == 0) + return; + + class = E_TEXT_MODEL_GET_CLASS (model); + + if (class->insert_length != NULL) + class->insert_length (model, position, text, length); +} + +void +e_text_model_prepend (ETextModel *model, + const gchar *text) +{ + g_return_if_fail (E_IS_TEXT_MODEL (model)); + + if (text == NULL) + return; + + e_text_model_insert (model, 0, text); +} + +void +e_text_model_append (ETextModel *model, + const gchar *text) +{ + g_return_if_fail (E_IS_TEXT_MODEL (model)); + + if (text == NULL) + return; + + e_text_model_insert (model, e_text_model_get_text_length (model), text); +} + +void +e_text_model_delete (ETextModel *model, + gint position, + gint length) +{ + ETextModelClass *class; + gint txt_len; + + g_return_if_fail (E_IS_TEXT_MODEL (model)); + g_return_if_fail (length >= 0); + + txt_len = e_text_model_get_text_length (model); + if (position + length > txt_len) + length = txt_len - position; + + if (length <= 0) + return; + + class = E_TEXT_MODEL_GET_CLASS (model); + + if (class->delete != NULL) + class->delete (model, position, length); +} + +gint +e_text_model_object_count (ETextModel *model) +{ + ETextModelClass *class; + + g_return_val_if_fail (E_IS_TEXT_MODEL (model), 0); + + class = E_TEXT_MODEL_GET_CLASS (model); + + if (class->obj_count == NULL) + return 0; + + return class->obj_count (model); +} + +const gchar * +e_text_model_get_nth_object (ETextModel *model, + gint n, + gint *len) +{ + ETextModelClass *class; + + g_return_val_if_fail (E_IS_TEXT_MODEL (model), NULL); + + if (n < 0 || n >= e_text_model_object_count (model)) + return NULL; + + class = E_TEXT_MODEL_GET_CLASS (model); + + if (class->get_nth_obj == NULL) + return NULL; + + return class->get_nth_obj (model, n, len); +} + +gchar * +e_text_model_strdup_nth_object (ETextModel *model, + gint n) +{ + const gchar *obj; + gint len = 0; + + g_return_val_if_fail (E_IS_TEXT_MODEL (model), NULL); + + obj = e_text_model_get_nth_object (model, n, &len); + + if (obj) { + gint byte_len; + byte_len = g_utf8_offset_to_pointer (obj, len) - obj; + return g_strndup (obj, byte_len); + } + else { + return NULL; + } +} + +void +e_text_model_get_nth_object_bounds (ETextModel *model, + gint n, + gint *start, + gint *end) +{ + const gchar *txt = NULL, *obj = NULL; + gint len = 0; + + g_return_if_fail (E_IS_TEXT_MODEL (model)); + + txt = e_text_model_get_text (model); + obj = e_text_model_get_nth_object (model, n, &len); + + g_return_if_fail (obj != NULL); + + if (start) + *start = g_utf8_pointer_to_offset (txt, obj); + if (end) + *end = (start ? *start : 0) + len; +} + +gint +e_text_model_get_object_at_offset (ETextModel *model, + gint offset) +{ + ETextModelClass *class; + + g_return_val_if_fail (E_IS_TEXT_MODEL (model), -1); + + if (offset < 0 || offset >= e_text_model_get_text_length (model)) + return -1; + + class = E_TEXT_MODEL_GET_CLASS (model); + + /* If an optimized version has been provided, we use it. */ + if (class->obj_at_offset != NULL) { + return class->obj_at_offset (model, offset); + + } else { + /* If not, we fake it.*/ + + gint i, N, pos0, pos1; + + N = e_text_model_object_count (model); + + for (i = 0; i < N; ++i) { + e_text_model_get_nth_object_bounds (model, i, &pos0, &pos1); + if (pos0 <= offset && offset < pos1) + return i; + } + + } + + return -1; +} + +gint +e_text_model_get_object_at_pointer (ETextModel *model, + const gchar *s) +{ + g_return_val_if_fail (E_IS_TEXT_MODEL (model), -1); + g_return_val_if_fail (s != NULL, -1); + + return e_text_model_get_object_at_offset ( + model, s - e_text_model_get_text (model)); +} + +void +e_text_model_activate_nth_object (ETextModel *model, + gint n) +{ + g_return_if_fail (model != NULL); + g_return_if_fail (E_IS_TEXT_MODEL (model)); + g_return_if_fail (n >= 0); + g_return_if_fail (n < e_text_model_object_count (model)); + + g_signal_emit (model, signals[E_TEXT_MODEL_OBJECT_ACTIVATED], 0, n); +} + +ETextModel * +e_text_model_new (void) +{ + ETextModel *model = g_object_new (E_TYPE_TEXT_MODEL, NULL); + return model; +} diff --git a/e-util/e-text-model.h b/e-util/e-text-model.h new file mode 100644 index 0000000000..3426c183e2 --- /dev/null +++ b/e-util/e-text-model.h @@ -0,0 +1,112 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_TEXT_MODEL_H +#define E_TEXT_MODEL_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define E_TYPE_TEXT_MODEL (e_text_model_get_type ()) +#define E_TEXT_MODEL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_TYPE_TEXT_MODEL, ETextModel)) +#define E_TEXT_MODEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), E_TYPE_TEXT_MODEL, ETextModelClass)) +#define E_IS_TEXT_MODEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_TEXT_MODEL)) +#define E_IS_TEXT_MODEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), E_TYPE_TEXT_MODEL)) +#define E_TEXT_MODEL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), E_TYPE_TEXT_MODEL_TYPE, ETextModelClass)) + +typedef struct _ETextModel ETextModel; +typedef struct _ETextModelClass ETextModelClass; +typedef struct _ETextModelPrivate ETextModelPrivate; + +typedef gint (*ETextModelReposFn) (gint, gpointer); + +struct _ETextModel { + GObject item; + + ETextModelPrivate *priv; +}; + +struct _ETextModelClass { + GObjectClass parent_class; + + /* Signal */ + void (* changed) (ETextModel *model); + void (* reposition) (ETextModel *model, ETextModelReposFn fn, gpointer repos_fn_data); + void (* object_activated) (ETextModel *model, gint obj_num); + void (* cancel_completion) (ETextModel *model); + + /* Virtual methods */ + + gint (* validate_pos) (ETextModel *model, gint pos); + + const gchar *(* get_text) (ETextModel *model); + gint (* get_text_len) (ETextModel *model); + void (* set_text) (ETextModel *model, const gchar *text); + void (* insert) (ETextModel *model, gint position, const gchar *text); + void (* insert_length) (ETextModel *model, gint position, const gchar *text, gint length); + void (* delete) (ETextModel *model, gint position, gint length); + + void (* objectify) (ETextModel *model); + gint (* obj_count) (ETextModel *model); + const gchar *(* get_nth_obj) (ETextModel *model, gint n, gint *len); + gint (* obj_at_offset) (ETextModel *model, gint offset); +}; + +GType e_text_model_get_type (void); + +ETextModel *e_text_model_new (void); + +void e_text_model_changed (ETextModel *model); +void e_text_model_cancel_completion (ETextModel *model); + +void e_text_model_reposition (ETextModel *model, ETextModelReposFn fn, gpointer repos_data); +gint e_text_model_validate_position (ETextModel *model, gint pos); + +/* Functions for manipulating the underlying text. */ + +const gchar *e_text_model_get_text (ETextModel *model); +gint e_text_model_get_text_length (ETextModel *model); +void e_text_model_set_text (ETextModel *model, const gchar *text); +void e_text_model_insert (ETextModel *model, gint position, const gchar *text); +void e_text_model_insert_length (ETextModel *model, gint position, const gchar *text, gint length); +void e_text_model_prepend (ETextModel *model, const gchar *text); +void e_text_model_append (ETextModel *model, const gchar *text); +void e_text_model_delete (ETextModel *model, gint position, gint length); + +/* Functions for accessing embedded objects. */ + +gint e_text_model_object_count (ETextModel *model); +const gchar *e_text_model_get_nth_object (ETextModel *model, gint n, gint *len); +gchar *e_text_model_strdup_nth_object (ETextModel *model, gint n); +void e_text_model_get_nth_object_bounds (ETextModel *model, gint n, gint *start_pos, gint *end_pos); +gint e_text_model_get_object_at_offset (ETextModel *model, gint offset); +gint e_text_model_get_object_at_pointer (ETextModel *model, const gchar *c); +void e_text_model_activate_nth_object (ETextModel *model, gint n); + +G_END_DECLS + +#endif diff --git a/e-util/e-text.c b/e-util/e-text.c new file mode 100644 index 0000000000..d23decad86 --- /dev/null +++ b/e-util/e-text.c @@ -0,0 +1,3405 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * e-text.c - Text item for evolution. + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Jon Trowbridge <trow@ximian.com> + * + * A majority of code taken from: + * + * Text item type for GnomeCanvas widget + * + * GnomeCanvas is basically a port of the Tk toolkit's most excellent + * canvas widget. Tk is copyrighted by the Regents of the University + * of California, Sun Microsystems, and other parties. + * + * Copyright (C) 1998 The Free Software Foundation + * + * Author: Federico Mena <federico@nuclecu.unam.mx> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-text.h" + +#include <math.h> +#include <ctype.h> +#include <string.h> + +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> + +#include "e-canvas-utils.h" +#include "e-canvas.h" +#include "e-marshal.h" +#include "e-text-event-processor-emacs-like.h" +#include "e-unicode.h" +#include "gal-a11y-e-text.h" + +G_DEFINE_TYPE (EText, e_text, GNOME_TYPE_CANVAS_ITEM) + +enum { + E_TEXT_CHANGED, + E_TEXT_ACTIVATE, + E_TEXT_KEYPRESS, + E_TEXT_POPULATE_POPUP, + E_TEXT_LAST_SIGNAL +}; + +static GQuark e_text_signals[E_TEXT_LAST_SIGNAL] = { 0 }; + +/* Object argument IDs */ +enum { + PROP_0, + PROP_MODEL, + PROP_EVENT_PROCESSOR, + PROP_TEXT, + PROP_BOLD, + PROP_STRIKEOUT, + PROP_ANCHOR, + PROP_JUSTIFICATION, + PROP_CLIP_WIDTH, + PROP_CLIP_HEIGHT, + PROP_CLIP, + PROP_FILL_CLIP_RECTANGLE, + PROP_X_OFFSET, + PROP_Y_OFFSET, + PROP_FILL_COLOR, + PROP_FILL_COLOR_GDK, + PROP_FILL_COLOR_RGBA, + PROP_TEXT_WIDTH, + PROP_TEXT_HEIGHT, + PROP_EDITABLE, + PROP_USE_ELLIPSIS, + PROP_ELLIPSIS, + PROP_LINE_WRAP, + PROP_BREAK_CHARACTERS, + PROP_MAX_LINES, + PROP_WIDTH, + PROP_HEIGHT, + PROP_ALLOW_NEWLINES, + PROP_CURSOR_POS, + PROP_IM_CONTEXT, + PROP_HANDLE_POPUP +}; + +static void e_text_command (ETextEventProcessor *tep, + ETextEventProcessorCommand *command, + gpointer data); + +static void e_text_text_model_changed (ETextModel *model, + EText *text); +static void e_text_text_model_reposition (ETextModel *model, + ETextModelReposFn fn, + gpointer repos_data, + gpointer data); + +static void _get_tep (EText *text); + +static void calc_height (EText *text); + +static gboolean show_pango_rectangle (EText *text, PangoRectangle rect); + +static void e_text_do_popup (EText *text, + GdkEvent *event_button, + gint position); + +static void e_text_update_primary_selection (EText *text); +static void e_text_paste (EText *text, GdkAtom selection); +static void e_text_insert (EText *text, const gchar *string); +static void e_text_reset_im_context (EText *text); + +static void reset_layout_attrs (EText *text); + +/* IM Context Callbacks */ +static void e_text_commit_cb (GtkIMContext *context, + const gchar *str, + EText *text); +static void e_text_preedit_changed_cb (GtkIMContext *context, + EText *text); +static gboolean e_text_retrieve_surrounding_cb (GtkIMContext *context, + EText *text); +static gboolean e_text_delete_surrounding_cb (GtkIMContext *context, + gint offset, + gint n_chars, + EText *text); + +static GdkAtom clipboard_atom = GDK_NONE; + +static void +disconnect_im_context (EText *text) +{ + if (!text || !text->im_context) + return; + + g_signal_handlers_disconnect_matched ( + text->im_context, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, text); + text->im_context_signals_registered = FALSE; +} + +/* Dispose handler for the text item */ +static void +e_text_dispose (GObject *object) +{ + EText *text; + + g_return_if_fail (object != NULL); + g_return_if_fail (E_IS_TEXT (object)); + + text = E_TEXT (object); + + if (text->model_changed_signal_id) + g_signal_handler_disconnect ( + text->model, + text->model_changed_signal_id); + text->model_changed_signal_id = 0; + + if (text->model_repos_signal_id) + g_signal_handler_disconnect ( + text->model, + text->model_repos_signal_id); + text->model_repos_signal_id = 0; + + if (text->model) + g_object_unref (text->model); + text->model = NULL; + + if (text->tep_command_id) + g_signal_handler_disconnect ( + text->tep, + text->tep_command_id); + text->tep_command_id = 0; + + if (text->tep) + g_object_unref (text->tep); + text->tep = NULL; + + g_free (text->revert); + text->revert = NULL; + + if (text->timeout_id) { + g_source_remove (text->timeout_id); + text->timeout_id = 0; + } + + if (text->timer) { + g_timer_stop (text->timer); + g_timer_destroy (text->timer); + text->timer = NULL; + } + + if (text->dbl_timeout) { + g_source_remove (text->dbl_timeout); + text->dbl_timeout = 0; + } + + if (text->tpl_timeout) { + g_source_remove (text->tpl_timeout); + text->tpl_timeout = 0; + } + + if (text->layout) { + g_object_unref (text->layout); + text->layout = NULL; + } + + if (text->im_context) { + disconnect_im_context (text); + g_object_unref (text->im_context); + text->im_context = NULL; + } + + if (text->font_desc) { + pango_font_description_free (text->font_desc); + text->font_desc = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_text_parent_class)->dispose (object); +} + +static void +insert_preedit_text (EText *text) +{ + PangoAttrList *attrs = NULL; + PangoAttrList *preedit_attrs = NULL; + gchar *preedit_string = NULL; + GString *tmp_string = g_string_new (NULL); + gint length = 0, cpos = 0; + gboolean new_attrs = FALSE; + + if (text->layout == NULL || !GTK_IS_IM_CONTEXT (text->im_context)) + return; + + text->text = e_text_model_get_text (text->model); + length = strlen (text->text); + + g_string_prepend_len (tmp_string, text->text,length); + + /* we came into this function only when text->preedit_len was not 0 + * so we can safely fetch the preedit string */ + gtk_im_context_get_preedit_string ( + text->im_context, &preedit_string, &preedit_attrs, NULL); + + if (preedit_string && g_utf8_validate (preedit_string, -1, NULL)) { + + text->preedit_len = strlen (preedit_string); + + cpos = g_utf8_offset_to_pointer ( + text->text, text->selection_start) - text->text; + + g_string_insert (tmp_string, cpos, preedit_string); + + reset_layout_attrs (text); + + attrs = pango_layout_get_attributes (text->layout); + if (!attrs) { + attrs = pango_attr_list_new (); + new_attrs = TRUE; + } + + pango_layout_set_text (text->layout, tmp_string->str, tmp_string->len); + + pango_attr_list_splice (attrs, preedit_attrs, cpos, text->preedit_len); + + if (new_attrs) { + pango_layout_set_attributes (text->layout, attrs); + pango_attr_list_unref (attrs); + } + } else + text->preedit_len = 0; + + if (preedit_string) + g_free (preedit_string); + if (preedit_attrs) + pango_attr_list_unref (preedit_attrs); + if (tmp_string) + g_string_free (tmp_string, TRUE); +} + +static void +reset_layout_attrs (EText *text) +{ + PangoAttrList *attrs = NULL; + gint object_count; + + if (text->layout == NULL) + return; + + object_count = e_text_model_object_count (text->model); + + if (text->bold || text->strikeout || object_count > 0) { + gint length = 0; + gint i; + + attrs = pango_attr_list_new (); + + for (i = 0; i < object_count; i++) { + gint start_pos, end_pos; + PangoAttribute *attr; + + attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE); + + e_text_model_get_nth_object_bounds ( + text->model, i, &start_pos, &end_pos); + + attr->start_index = g_utf8_offset_to_pointer ( + text->text, start_pos) - text->text; + attr->end_index = g_utf8_offset_to_pointer ( + text->text, end_pos) - text->text; + + pango_attr_list_insert (attrs, attr); + } + + if (text->bold || text->strikeout) + length = strlen (text->text); + + if (text->bold) { + PangoAttribute *attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD); + attr->start_index = 0; + attr->end_index = length; + + pango_attr_list_insert_before (attrs, attr); + } + if (text->strikeout) { + PangoAttribute *attr = pango_attr_strikethrough_new (TRUE); + attr->start_index = 0; + attr->end_index = length; + + pango_attr_list_insert_before (attrs, attr); + } + } + + pango_layout_set_attributes (text->layout, attrs); + + if (attrs) + pango_attr_list_unref (attrs); + + calc_height (text); +} + +static void +create_layout (EText *text) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (text); + + if (text->layout) + return; + + text->layout = gtk_widget_create_pango_layout ( + GTK_WIDGET (item->canvas), text->text); + if (text->line_wrap) + pango_layout_set_width ( + text->layout, text->clip_width < 0 + ? -1 : text->clip_width * PANGO_SCALE); + reset_layout_attrs (text); +} + +static void +reset_layout (EText *text) +{ + GnomeCanvasItem *item = GNOME_CANVAS_ITEM (text); + + if (text->layout == NULL) { + create_layout (text); + } + else { + GtkStyle *style; + + style = gtk_widget_get_style (GTK_WIDGET (item->canvas)); + + if (text->font_desc) { + pango_font_description_free (text->font_desc); + } + text->font_desc = pango_font_description_new (); + if (!pango_font_description_get_size_is_absolute (style->font_desc)) + pango_font_description_set_size ( + text->font_desc, + pango_font_description_get_size (style->font_desc)); + else + pango_font_description_set_absolute_size ( + text->font_desc, + pango_font_description_get_size (style->font_desc)); + pango_font_description_set_family ( + text->font_desc, + pango_font_description_get_family (style->font_desc)); + pango_layout_set_font_description (text->layout, text->font_desc); + + pango_layout_set_text (text->layout, text->text, -1); + reset_layout_attrs (text); + } + + if (!text->button_down) { + PangoRectangle strong_pos, weak_pos; + gchar *offs = g_utf8_offset_to_pointer (text->text, text->selection_start); + + pango_layout_get_cursor_pos ( + text->layout, offs - text->text, + &strong_pos, &weak_pos); + + if (strong_pos.x != weak_pos.x || + strong_pos.y != weak_pos.y || + strong_pos.width != weak_pos.width || + strong_pos.height != weak_pos.height) + show_pango_rectangle (text, weak_pos); + + show_pango_rectangle (text, strong_pos); + } +} + +static void +e_text_text_model_changed (ETextModel *model, + EText *text) +{ + gint model_len = e_text_model_get_text_length (model); + text->text = e_text_model_get_text (model); + + /* Make sure our selection doesn't extend past the bounds of our text. */ + text->selection_start = CLAMP (text->selection_start, 0, model_len); + text->selection_end = CLAMP (text->selection_end, 0, model_len); + + text->needs_reset_layout = 1; + text->needs_split_into_lines = 1; + text->needs_redraw = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (text)); + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (text)); + + g_signal_emit (text, e_text_signals[E_TEXT_CHANGED], 0); +} + +static void +e_text_text_model_reposition (ETextModel *model, + ETextModelReposFn fn, + gpointer repos_data, + gpointer user_data) +{ + EText *text = E_TEXT (user_data); + gint model_len = e_text_model_get_text_length (model); + + text->selection_start = fn (text->selection_start, repos_data); + text->selection_end = fn (text->selection_end, repos_data); + + /* Our repos function should make sure we don't overrun the buffer, but it never + * hurts to be paranoid. */ + text->selection_start = CLAMP (text->selection_start, 0, model_len); + text->selection_end = CLAMP (text->selection_end, 0, model_len); + + if (text->selection_start > text->selection_end) { + gint tmp = text->selection_start; + text->selection_start = text->selection_end; + text->selection_end = tmp; + } +} + +static void +get_bounds (EText *text, + gdouble *px1, + gdouble *py1, + gdouble *px2, + gdouble *py2) +{ + GnomeCanvasItem *item; + gdouble wx, wy, clip_width, clip_height; + + item = GNOME_CANVAS_ITEM (text); + + /* Get canvas pixel coordinates for text position */ + + wx = 0; + wy = 0; + gnome_canvas_item_i2w (item, &wx, &wy); + gnome_canvas_w2c (item->canvas, wx, wy, &text->cx, &text->cy); + gnome_canvas_w2c (item->canvas, wx, wy, &text->clip_cx, &text->clip_cy); + + if (text->clip_width < 0) + clip_width = text->width; + else + clip_width = text->clip_width; + + if (text->clip_height < 0) + clip_height = text->height; + else + clip_height = text->clip_height; + + /* Get canvas pixel coordinates for clip rectangle position */ + text->clip_cwidth = clip_width; + text->clip_cheight = clip_height; + + text->text_cx = text->cx; + text->text_cy = text->cy; + + /* Bounds */ + + if (text->clip) { + *px1 = text->clip_cx; + *py1 = text->clip_cy; + *px2 = text->clip_cx + text->clip_cwidth; + *py2 = text->clip_cy + text->clip_cheight; + } else { + *px1 = text->cx; + *py1 = text->cy; + *px2 = text->cx + text->width; + *py2 = text->cy + text->height; + } +} + +static void +calc_height (EText *text) +{ + GnomeCanvasItem *item; + gint old_height; + gint old_width; + gint width = 0; + gint height = 0; + + item = GNOME_CANVAS_ITEM (text); + + /* Calculate text dimensions */ + + old_height = text->height; + old_width = text->width; + + if (text->layout) + pango_layout_get_pixel_size (text->layout, &width, &height); + + text->height = height; + text->width = width; + + if (old_height != text->height || old_width != text->width) + e_canvas_item_request_parent_reflow (item); +} + +static void +calc_ellipsis (EText *text) +{ +/* FIXME: a pango layout per calc_ellipsis sucks */ + gint width; + PangoLayout *layout = gtk_widget_create_pango_layout ( + GTK_WIDGET (GNOME_CANVAS_ITEM (text)->canvas), + text->ellipsis ? text->ellipsis : "..."); + pango_layout_get_size (layout, &width, NULL); + + text->ellipsis_width = width; + + g_object_unref (layout); +} + +static void +split_into_lines (EText *text) +{ + text->num_lines = pango_layout_get_line_count (text->layout); +} + +/* Set_arg handler for the text item */ +static void +e_text_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GnomeCanvasItem *item; + EText *text; + GdkColor color = { 0, 0, 0, 0, }; + GdkColor *pcolor; + + gboolean needs_update = 0; + gboolean needs_reflow = 0; + + item = GNOME_CANVAS_ITEM (object); + text = E_TEXT (object); + + switch (property_id) { + case PROP_MODEL: + + if (text->model_changed_signal_id) + g_signal_handler_disconnect ( + text->model, + text->model_changed_signal_id); + + if (text->model_repos_signal_id) + g_signal_handler_disconnect ( + text->model, + text->model_repos_signal_id); + + g_object_unref (text->model); + text->model = E_TEXT_MODEL (g_value_get_object (value)); + g_object_ref (text->model); + + text->model_changed_signal_id = g_signal_connect ( + text->model, "changed", + G_CALLBACK (e_text_text_model_changed), text); + + text->model_repos_signal_id = g_signal_connect ( + text->model, "reposition", + G_CALLBACK (e_text_text_model_reposition), text); + + text->text = e_text_model_get_text (text->model); + g_signal_emit (text, e_text_signals[E_TEXT_CHANGED], 0); + + text->needs_split_into_lines = 1; + needs_reflow = 1; + break; + + case PROP_EVENT_PROCESSOR: + if (text->tep && text->tep_command_id) + g_signal_handler_disconnect ( + text->tep, + text->tep_command_id); + if (text->tep) { + g_object_unref (text->tep); + } + text->tep = E_TEXT_EVENT_PROCESSOR (g_value_get_object (value)); + g_object_ref (text->tep); + + text->tep_command_id = g_signal_connect ( + text->tep, "command", + G_CALLBACK (e_text_command), text); + + if (!text->allow_newlines) + g_object_set ( + text->tep, + "allow_newlines", FALSE, + NULL); + break; + + case PROP_TEXT: + e_text_model_set_text (text->model, g_value_get_string (value)); + break; + + case PROP_BOLD: + text->bold = g_value_get_boolean (value); + + text->needs_redraw = 1; + text->needs_recalc_bounds = 1; + if (text->line_wrap) + text->needs_split_into_lines = 1; + else { + text->needs_calc_height = 1; + } + needs_update = 1; + needs_reflow = 1; + break; + + case PROP_STRIKEOUT: + text->strikeout = g_value_get_boolean (value); + text->needs_redraw = 1; + needs_update = 1; + break; + + case PROP_JUSTIFICATION: + text->justification = g_value_get_enum (value); + text->needs_redraw = 1; + needs_update = 1; + break; + + case PROP_CLIP_WIDTH: + text->clip_width = fabs (g_value_get_double (value)); + calc_ellipsis (text); + if (text->line_wrap) { + if (text->layout) + pango_layout_set_width ( + text->layout, text->clip_width < 0 + ? -1 : text->clip_width * PANGO_SCALE); + text->needs_split_into_lines = 1; + } else { + text->needs_calc_height = 1; + } + needs_reflow = 1; + break; + + case PROP_CLIP_HEIGHT: + text->clip_height = fabs (g_value_get_double (value)); + text->needs_recalc_bounds = 1; + /* toshok: kind of a hack - set needs_reset_layout + * here so when something about the style/them + * changes, we redraw the text at the proper size/with + * the proper font. */ + text->needs_reset_layout = 1; + needs_reflow = 1; + break; + + case PROP_CLIP: + text->clip = g_value_get_boolean (value); + calc_ellipsis (text); + if (text->line_wrap) + text->needs_split_into_lines = 1; + else { + text->needs_calc_height = 1; + } + needs_reflow = 1; + break; + + case PROP_FILL_CLIP_RECTANGLE: + text->fill_clip_rectangle = g_value_get_boolean (value); + needs_update = 1; + break; + + case PROP_X_OFFSET: + text->xofs = g_value_get_double (value); + text->needs_recalc_bounds = 1; + needs_update = 1; + break; + + case PROP_Y_OFFSET: + text->yofs = g_value_get_double (value); + text->needs_recalc_bounds = 1; + needs_update = 1; + break; + + case PROP_FILL_COLOR: + if (g_value_get_string (value)) + gdk_color_parse (g_value_get_string (value), &color); + + text->rgba = ((color.red & 0xff00) << 16 | + (color.green & 0xff00) << 8 | + (color.blue & 0xff00) | + 0xff); + text->rgba_set = TRUE; + text->needs_redraw = 1; + needs_update = 1; + break; + + case PROP_FILL_COLOR_GDK: + pcolor = g_value_get_boxed (value); + if (pcolor) { + color = *pcolor; + } + + text->rgba = ((color.red & 0xff00) << 16 | + (color.green & 0xff00) << 8 | + (color.blue & 0xff00) | + 0xff); + text->rgba_set = TRUE; + text->needs_redraw = 1; + needs_update = 1; + break; + + case PROP_FILL_COLOR_RGBA: + text->rgba = g_value_get_uint (value); + color.red = ((text->rgba >> 24) & 0xff) * 0x101; + color.green = ((text->rgba >> 16) & 0xff) * 0x101; + color.blue = ((text->rgba >> 8) & 0xff) * 0x101; + text->rgba_set = TRUE; + text->needs_redraw = 1; + needs_update = 1; + break; + + case PROP_EDITABLE: + text->editable = g_value_get_boolean (value); + text->needs_redraw = 1; + needs_update = 1; + break; + + case PROP_USE_ELLIPSIS: + text->use_ellipsis = g_value_get_boolean (value); + needs_reflow = 1; + break; + + case PROP_ELLIPSIS: + if (text->ellipsis) + g_free (text->ellipsis); + + text->ellipsis = g_strdup (g_value_get_string (value)); + calc_ellipsis (text); + needs_reflow = 1; + break; + + case PROP_LINE_WRAP: + text->line_wrap = g_value_get_boolean (value); + if (text->line_wrap) { + if (text->layout) { + pango_layout_set_width ( + text->layout, text->width < 0 + ? -1 : text->width * PANGO_SCALE); + } + } + text->needs_split_into_lines = 1; + needs_reflow = 1; + break; + + case PROP_BREAK_CHARACTERS: + if (text->break_characters) { + g_free (text->break_characters); + text->break_characters = NULL; + } + if (g_value_get_string (value)) + text->break_characters = g_strdup (g_value_get_string (value)); + text->needs_split_into_lines = 1; + needs_reflow = 1; + break; + + case PROP_MAX_LINES: + text->max_lines = g_value_get_int (value); + text->needs_split_into_lines = 1; + needs_reflow = 1; + break; + + case PROP_WIDTH: + text->clip_width = fabs (g_value_get_double (value)); + calc_ellipsis (text); + if (text->line_wrap) { + if (text->layout) { + pango_layout_set_width ( + text->layout, text->width < 0 ? + -1 : text->width * PANGO_SCALE); + } + text->needs_split_into_lines = 1; + } + else { + text->needs_calc_height = 1; + } + needs_reflow = 1; + break; + + case PROP_ALLOW_NEWLINES: + text->allow_newlines = g_value_get_boolean (value); + _get_tep (text); + g_object_set ( + text->tep, + "allow_newlines", g_value_get_boolean (value), + NULL); + break; + + case PROP_CURSOR_POS: { + ETextEventProcessorCommand command; + + command.action = E_TEP_MOVE; + command.position = E_TEP_VALUE; + command.value = g_value_get_int (value); + command.time = GDK_CURRENT_TIME; + e_text_command (text->tep, &command, text); + break; + } + + case PROP_IM_CONTEXT: + if (text->im_context) { + disconnect_im_context (text); + g_object_unref (text->im_context); + } + + text->im_context = g_value_get_object (value); + if (text->im_context) + g_object_ref (text->im_context); + + text->need_im_reset = TRUE; + break; + + case PROP_HANDLE_POPUP: + text->handle_popup = g_value_get_boolean (value); + break; + + default: + return; + } + + if (needs_reflow) + e_canvas_item_request_reflow (item); + if (needs_update) + gnome_canvas_item_request_update (item); +} + +/* Get_arg handler for the text item */ +static void +e_text_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EText *text; + + text = E_TEXT (object); + + switch (property_id) { + case PROP_MODEL: + g_value_set_object (value, text->model); + break; + + case PROP_EVENT_PROCESSOR: + _get_tep (text); + g_value_set_object (value, text->tep); + break; + + case PROP_TEXT: + g_value_set_string (value, text->text); + break; + + case PROP_BOLD: + g_value_set_boolean (value, text->bold); + break; + + case PROP_STRIKEOUT: + g_value_set_boolean (value, text->strikeout); + break; + + case PROP_JUSTIFICATION: + g_value_set_enum (value, text->justification); + break; + + case PROP_CLIP_WIDTH: + g_value_set_double (value, text->clip_width); + break; + + case PROP_CLIP_HEIGHT: + g_value_set_double (value, text->clip_height); + break; + + case PROP_CLIP: + g_value_set_boolean (value, text->clip); + break; + + case PROP_FILL_CLIP_RECTANGLE: + g_value_set_boolean (value, text->fill_clip_rectangle); + break; + + case PROP_X_OFFSET: + g_value_set_double (value, text->xofs); + break; + + case PROP_Y_OFFSET: + g_value_set_double (value, text->yofs); + break; + + case PROP_FILL_COLOR_RGBA: + g_value_set_uint (value, text->rgba); + break; + + case PROP_TEXT_WIDTH: + g_value_set_double (value, text->width); + break; + + case PROP_TEXT_HEIGHT: + g_value_set_double (value, text->height); + break; + + case PROP_EDITABLE: + g_value_set_boolean (value, text->editable); + break; + + case PROP_USE_ELLIPSIS: + g_value_set_boolean (value, text->use_ellipsis); + break; + + case PROP_ELLIPSIS: + g_value_set_string (value, text->ellipsis); + break; + + case PROP_LINE_WRAP: + g_value_set_boolean (value, text->line_wrap); + break; + + case PROP_BREAK_CHARACTERS: + g_value_set_string (value, text->break_characters); + break; + + case PROP_MAX_LINES: + g_value_set_int (value, text->max_lines); + break; + + case PROP_WIDTH: + g_value_set_double (value, text->clip_width); + break; + + case PROP_HEIGHT: + g_value_set_double ( + value, text->clip && + text->clip_height != -1 ? + text->clip_height : text->height); + break; + + case PROP_ALLOW_NEWLINES: + g_value_set_boolean (value, text->allow_newlines); + break; + + case PROP_CURSOR_POS: + g_value_set_int (value, text->selection_start); + break; + + case PROP_IM_CONTEXT: + g_value_set_object (value, text->im_context); + break; + + case PROP_HANDLE_POPUP: + g_value_set_boolean (value, text->handle_popup); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +/* Update handler for the text item */ +static void +e_text_reflow (GnomeCanvasItem *item, + gint flags) +{ + EText *text; + + text = E_TEXT (item); + + if (text->needs_reset_layout) { + reset_layout (text); + text->needs_reset_layout = 0; + text->needs_calc_height = 1; + } + + if (text->needs_split_into_lines) { + split_into_lines (text); + + text->needs_split_into_lines = 0; + text->needs_calc_height = 1; + } + + if (text->needs_calc_height) { + calc_height (text); + gnome_canvas_item_request_update (item); + text->needs_calc_height = 0; + text->needs_recalc_bounds = 1; + } +} + +/* Update handler for the text item */ +static void +e_text_update (GnomeCanvasItem *item, + const cairo_matrix_t *i2c, + gint flags) +{ + EText *text; + gdouble x1, y1, x2, y2; + + text = E_TEXT (item); + + if (GNOME_CANVAS_ITEM_CLASS (e_text_parent_class)->update) + GNOME_CANVAS_ITEM_CLASS (e_text_parent_class)->update ( + item, i2c, flags); + + if (text->needs_recalc_bounds + || (flags & GNOME_CANVAS_UPDATE_AFFINE)) { + get_bounds (text, &x1, &y1, &x2, &y2); + if (item->x1 != x1 || + item->x2 != x2 || + item->y1 != y1 || + item->y2 != y2) { + gnome_canvas_request_redraw ( + item->canvas, item->x1, item->y1, + item->x2, item->y2); + item->x1 = x1; + item->y1 = y1; + item->x2 = x2; + item->y2 = y2; + text->needs_redraw = 1; + item->canvas->need_repick = TRUE; + } + if (!text->fill_clip_rectangle) + item->canvas->need_repick = TRUE; + text->needs_recalc_bounds = 0; + } + if (text->needs_redraw) { + gnome_canvas_request_redraw ( + item->canvas, item->x1, item->y1, item->x2, item->y2); + text->needs_redraw = 0; + } +} + +/* Realize handler for the text item */ +static void +e_text_realize (GnomeCanvasItem *item) +{ + EText *text; + + text = E_TEXT (item); + + if (GNOME_CANVAS_ITEM_CLASS (e_text_parent_class)->realize) + (* GNOME_CANVAS_ITEM_CLASS (e_text_parent_class)->realize) (item); + + create_layout (text); + + text->i_cursor = gdk_cursor_new (GDK_XTERM); + text->default_cursor = gdk_cursor_new (GDK_LEFT_PTR); +} + +/* Unrealize handler for the text item */ +static void +e_text_unrealize (GnomeCanvasItem *item) +{ + EText *text; + + text = E_TEXT (item); + + g_object_unref (text->i_cursor); + text->i_cursor = NULL; + g_object_unref (text->default_cursor); + text->default_cursor = NULL; + + if (GNOME_CANVAS_ITEM_CLASS (e_text_parent_class)->unrealize) + (* GNOME_CANVAS_ITEM_CLASS (e_text_parent_class)->unrealize) (item); +} + +static void +_get_tep (EText *text) +{ + if (!text->tep) { + text->tep = e_text_event_processor_emacs_like_new (); + + text->tep_command_id = g_signal_connect ( + text->tep, "command", + G_CALLBACK (e_text_command), text); + } +} + +static void +draw_pango_rectangle (cairo_t *cr, + gint x1, + gint y1, + PangoRectangle rect) +{ + gint width = rect.width / PANGO_SCALE; + gint height = rect.height / PANGO_SCALE; + + if (width <= 0) + width = 1; + if (height <= 0) + height = 1; + + cairo_rectangle ( + cr, x1 + rect.x / PANGO_SCALE, + y1 + rect.y / PANGO_SCALE, width, height); + cairo_fill (cr); +} + +static gboolean +show_pango_rectangle (EText *text, + PangoRectangle rect) +{ + gint x1 = rect.x / PANGO_SCALE; + gint x2 = (rect.x + rect.width) / PANGO_SCALE; + + gint y1 = rect.y / PANGO_SCALE; + gint y2 = (rect.y + rect.height) / PANGO_SCALE; + + gint new_xofs_edit = text->xofs_edit; + gint new_yofs_edit = text->yofs_edit; + + gint clip_width, clip_height; + + clip_width = text->clip_width; + clip_height = text->clip_height; + + if (x1 < new_xofs_edit) + new_xofs_edit = x1; + + if (y1 < new_yofs_edit) + new_yofs_edit = y1; + + if (clip_width >= 0) { + if (2 + x2 - clip_width > new_xofs_edit) + new_xofs_edit = 2 + x2 - clip_width; + } else { + new_xofs_edit = 0; + } + + if (clip_height >= 0) { + if (y2 - clip_height > new_yofs_edit) + new_yofs_edit = y2 - clip_height; + } else { + new_yofs_edit = 0; + } + + if (new_xofs_edit < 0) + new_xofs_edit = 0; + if (new_yofs_edit < 0) + new_yofs_edit = 0; + + if (new_xofs_edit != text->xofs_edit || + new_yofs_edit != text->yofs_edit) { + text->xofs_edit = new_xofs_edit; + text->yofs_edit = new_yofs_edit; + return TRUE; + } + + return FALSE; +} + +/* Draw handler for the text item */ +static void +e_text_draw (GnomeCanvasItem *item, + cairo_t *cr, + gint x, + gint y, + gint width, + gint height) +{ + EText *text; + gint xpos, ypos; + GnomeCanvas *canvas; + GtkWidget *widget; + GtkStyle *style; + GtkStateType state; + + text = E_TEXT (item); + canvas = GNOME_CANVAS_ITEM (text)->canvas; + widget = GTK_WIDGET (canvas); + state = gtk_widget_get_state (widget); + style = gtk_widget_get_style (widget); + + cairo_save (cr); + + if (!text->rgba_set) { + gdk_cairo_set_source_color (cr, &style->fg[state]); + } else { + cairo_set_source_rgba ( + cr, + ((text->rgba >> 24) & 0xff) / 255.0, + ((text->rgba >> 16) & 0xff) / 255.0, + ((text->rgba >> 8) & 0xff) / 255.0, + ( text->rgba & 0xff) / 255.0); + } + + /* Insert preedit text only when im_context signals are connected & + * text->preedit_len is not zero */ + if (text->im_context_signals_registered && text->preedit_len) + insert_preedit_text (text); + + /* Need to reset the layout to cleanly clear the preedit buffer when + * typing in CJK & using backspace on the preedit */ + if (!text->preedit_len) + reset_layout (text); + + if (!pango_layout_get_text (text->layout)) { + cairo_restore (cr); + return; + } + + xpos = text->text_cx; + ypos = text->text_cy; + + xpos = xpos - x + text->xofs; + ypos = ypos - y + text->yofs; + + cairo_save (cr); + + if (text->clip) { + cairo_rectangle ( + cr, xpos, ypos, + text->clip_cwidth - text->xofs, + text->clip_cheight - text->yofs); + cairo_clip (cr); + } + + if (text->editing) { + xpos -= text->xofs_edit; + ypos -= text->yofs_edit; + } + + cairo_move_to (cr, xpos, ypos); + pango_cairo_show_layout (cr, text->layout); + + if (text->editing) { + if (text->selection_start != text->selection_end) { + cairo_region_t *clip_region = cairo_region_create (); + gint indices[2]; + GtkStateType state; + + state = GTK_STATE_ACTIVE; + + indices[0] = MIN ( + text->selection_start, + text->selection_end); + indices[1] = MAX ( + text->selection_start, + text->selection_end); + + /* convert these into byte indices */ + indices[0] = g_utf8_offset_to_pointer ( + text->text, indices[0]) - text->text; + indices[1] = g_utf8_offset_to_pointer ( + text->text, indices[1]) - text->text; + + clip_region = gdk_pango_layout_get_clip_region ( + text->layout, xpos, ypos, indices, 1); + gdk_cairo_region (cr, clip_region); + cairo_clip (cr); + cairo_region_destroy (clip_region); + + gdk_cairo_set_source_color (cr, &style->base[state]); + cairo_paint (cr); + + gdk_cairo_set_source_color (cr, &style->text[state]); + cairo_move_to (cr, xpos, ypos); + pango_cairo_show_layout (cr, text->layout); + } else { + if (text->show_cursor) { + PangoRectangle strong_pos, weak_pos; + gchar *offs; + + offs = g_utf8_offset_to_pointer ( + text->text, text->selection_start); + + pango_layout_get_cursor_pos ( + text->layout, offs - text->text + + text->preedit_len, &strong_pos, + &weak_pos); + draw_pango_rectangle (cr, xpos, ypos, strong_pos); + if (strong_pos.x != weak_pos.x || + strong_pos.y != weak_pos.y || + strong_pos.width != weak_pos.width || + strong_pos.height != weak_pos.height) + draw_pango_rectangle (cr, xpos, ypos, weak_pos); + } + } + } + + cairo_restore (cr); + cairo_restore (cr); +} + +/* Point handler for the text item */ +static GnomeCanvasItem * +e_text_point (GnomeCanvasItem *item, + gdouble x, + gdouble y, + gint cx, + gint cy) +{ + EText *text; + gdouble clip_width; + gdouble clip_height; + + text = E_TEXT (item); + + /* The idea is to build bounding rectangles for each of the lines of + * text (clipped by the clipping rectangle, if it is activated) and see + * whether the point is inside any of these. If it is, we are done. + * Otherwise, calculate the distance to the nearest rectangle. + */ + + if (text->clip_width < 0) + clip_width = text->width; + else + clip_width = text->clip_width; + + if (text->clip_height < 0) + clip_height = text->height; + else + clip_height = text->clip_height; + + if (cx < text->clip_cx || + cx > text->clip_cx + clip_width || + cy < text->clip_cy || + cy > text->clip_cy + clip_height) + return NULL; + + if (text->fill_clip_rectangle || !text->text || !*text->text) + return item; + + cx -= text->cx; + + if (pango_layout_xy_to_index (text->layout, cx, cy, NULL, NULL)) + return item; + + return NULL; +} + +/* Bounds handler for the text item */ +static void +e_text_bounds (GnomeCanvasItem *item, + gdouble *x1, + gdouble *y1, + gdouble *x2, + gdouble *y2) +{ + EText *text; + gdouble width, height; + + text = E_TEXT (item); + + *x1 = 0; + *y1 = 0; + + width = text->width; + height = text->height; + + if (text->clip) { + if (text->clip_width >= 0) + width = text->clip_width; + if (text->clip_height >= 0) + height = text->clip_height; + } + + *x2 = *x1 + width; + *y2 = *y1 + height; +} + +static gint +get_position_from_xy (EText *text, + gint x, + gint y) +{ + gint index; + gint trailing; + + x -= text->xofs; + y -= text->yofs; + + if (text->editing) { + x += text->xofs_edit; + y += text->yofs_edit; + } + + x -= text->cx; + y -= text->cy; + + pango_layout_xy_to_index ( + text->layout, x * PANGO_SCALE, + y * PANGO_SCALE, &index, &trailing); + + return g_utf8_pointer_to_offset (text->text, text->text + index + trailing); +} + +#define SCROLL_WAIT_TIME 30000 + +static gboolean +_blink_scroll_timeout (gpointer data) +{ + EText *text = E_TEXT (data); + gulong current_time; + gboolean scroll = FALSE; + gboolean redraw = FALSE; + + g_timer_elapsed (text->timer, ¤t_time); + + if (text->scroll_start + SCROLL_WAIT_TIME > 1000000) { + if (current_time > text->scroll_start - (1000000 - SCROLL_WAIT_TIME) && + current_time < text->scroll_start) + scroll = TRUE; + } else { + if (current_time > text->scroll_start + SCROLL_WAIT_TIME || + current_time < text->scroll_start) + scroll = TRUE; + } + if (scroll && text->button_down && text->clip) { + gint old_xofs_edit = text->xofs_edit; + gint old_yofs_edit = text->yofs_edit; + + if (text->clip_cwidth >= 0 && + text->lastx - text->clip_cx > text->clip_cwidth && + text->xofs_edit < text->width - text->clip_cwidth) { + text->xofs_edit += 4; + if (text->xofs_edit > text->width - text->clip_cwidth + 1) + text->xofs_edit = text->width - text->clip_cwidth + 1; + } + if (text->lastx - text->clip_cx < 0 && + text->xofs_edit > 0) { + text->xofs_edit -= 4; + if (text->xofs_edit < 0) + text->xofs_edit = 0; + } + + if (text->clip_cheight >= 0 && + text->lasty - text->clip_cy > text->clip_cheight && + text->yofs_edit < text->height - text->clip_cheight) { + text->yofs_edit += 4; + if (text->yofs_edit > text->height - text->clip_cheight + 1) + text->yofs_edit = text->height - text->clip_cheight + 1; + } + if (text->lasty - text->clip_cy < 0 && + text->yofs_edit > 0) { + text->yofs_edit -= 4; + if (text->yofs_edit < 0) + text->yofs_edit = 0; + } + + if (old_xofs_edit != text->xofs_edit || + old_yofs_edit != text->yofs_edit) { + ETextEventProcessorEvent e_tep_event; + e_tep_event.type = GDK_MOTION_NOTIFY; + e_tep_event.motion.state = text->last_state; + e_tep_event.motion.time = 0; + e_tep_event.motion.position = + get_position_from_xy ( + text, text->lastx, text->lasty); + _get_tep (text); + e_text_event_processor_handle_event ( + text->tep, + &e_tep_event); + text->scroll_start = current_time; + redraw = TRUE; + } + } + + if (!((current_time / 500000) % 2)) { + if (!text->show_cursor) + redraw = TRUE; + text->show_cursor = TRUE; + } else { + if (text->show_cursor) + redraw = TRUE; + text->show_cursor = FALSE; + } + if (redraw) { + text->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (text)); + } + return TRUE; +} + +static void +start_editing (EText *text) +{ + if (text->editing) + return; + + e_text_reset_im_context (text); + + g_free (text->revert); + text->revert = g_strdup (text->text); + + text->editing = TRUE; + if (text->pointer_in) { + GdkWindow *window; + + window = gtk_widget_get_window ( + GTK_WIDGET (GNOME_CANVAS_ITEM (text)->canvas)); + + if (text->default_cursor_shown) { + gdk_window_set_cursor (window, text->i_cursor); + text->default_cursor_shown = FALSE; + } + } + text->select_by_word = FALSE; + text->xofs_edit = 0; + text->yofs_edit = 0; + if (text->timeout_id == 0) + text->timeout_id = g_timeout_add (10, _blink_scroll_timeout, text); + text->timer = g_timer_new (); + g_timer_elapsed (text->timer, &(text->scroll_start)); + g_timer_start (text->timer); +} + +void +e_text_stop_editing (EText *text) +{ + if (!text->editing) + return; + + g_free (text->revert); + text->revert = NULL; + + text->editing = FALSE; + if (!text->default_cursor_shown) { + GdkWindow *window; + + window = gtk_widget_get_window ( + GTK_WIDGET (GNOME_CANVAS_ITEM (text)->canvas)); + gdk_window_set_cursor (window, text->default_cursor); + text->default_cursor_shown = TRUE; + } + if (text->timer) { + g_timer_stop (text->timer); + g_timer_destroy (text->timer); + text->timer = NULL; + } + + text->need_im_reset = TRUE; + text->preedit_len = 0; + text->preedit_pos = 0; +} + +void +e_text_cancel_editing (EText *text) +{ + if (text->revert) + e_text_model_set_text (text->model, text->revert); + e_text_stop_editing (text); +} + +static gboolean +_click (gpointer data) +{ + *(gint *)data = 0; + return FALSE; +} + +static gint +e_text_event (GnomeCanvasItem *item, + GdkEvent *event) +{ + EText *text = E_TEXT (item); + ETextEventProcessorEvent e_tep_event; + GdkWindow *window; + gint return_val = 0; + + if (!text->model) + return 0; + + window = gtk_widget_get_window (GTK_WIDGET (item->canvas)); + + e_tep_event.type = event->type; + switch (event->type) { + case GDK_FOCUS_CHANGE: + if (text->editable) { + GdkEventFocus *focus_event; + focus_event = (GdkEventFocus *) event; + if (focus_event->in) { + if (text->im_context) { + if (!text->im_context_signals_registered) { + g_signal_connect ( + text->im_context, "commit", + G_CALLBACK (e_text_commit_cb), text); + g_signal_connect ( + text->im_context, "preedit_changed", + G_CALLBACK (e_text_preedit_changed_cb), text); + g_signal_connect ( + text->im_context, "retrieve_surrounding", + G_CALLBACK (e_text_retrieve_surrounding_cb), text); + g_signal_connect ( + text->im_context, "delete_surrounding", + G_CALLBACK (e_text_delete_surrounding_cb), text); + text->im_context_signals_registered = TRUE; + } + gtk_im_context_focus_in (text->im_context); + } + + start_editing (text); + + /* So we'll redraw and the + * cursor will be shown. */ + text->show_cursor = FALSE; + } else { + if (text->im_context) { + gtk_im_context_focus_out (text->im_context); + disconnect_im_context (text); + text->need_im_reset = TRUE; + } + + e_text_stop_editing (text); + if (text->timeout_id) { + g_source_remove (text->timeout_id); + text->timeout_id = 0; + } + if (text->show_cursor) { + text->show_cursor = FALSE; + text->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (text)); + } + } + if (text->line_wrap) + text->needs_split_into_lines = 1; + e_canvas_item_request_reflow (GNOME_CANVAS_ITEM (text)); + } + return_val = 0; + break; + case GDK_KEY_PRESS: + + /* Handle S-F10 key binding here. */ + + if (event->key.keyval == GDK_KEY_F10 + && (event->key.state & GDK_SHIFT_MASK) + && text->handle_popup) { + + /* Simulate a GdkEventButton here, so that we can + * call e_text_do_popup directly */ + + GdkEvent *button_event; + + button_event = gdk_event_new (GDK_BUTTON_PRESS); + button_event->button.time = event->key.time; + button_event->button.button = 0; + e_text_do_popup (text, button_event, 0); + return 1; + } + + /* Fall Through */ + + case GDK_KEY_RELEASE: + + if (text->editing) { + GdkEventKey key; + gint ret; + + if (text->im_context && + gtk_im_context_filter_keypress ( + text->im_context, + (GdkEventKey *) event)) { + text->need_im_reset = TRUE; + return 1; + } + + key = event->key; + e_tep_event.key.time = key.time; + e_tep_event.key.state = key.state; + e_tep_event.key.keyval = key.keyval; + + /* This is probably ugly hack, but we + * have to handle UTF-8 input somehow. */ + e_tep_event.key.string = e_utf8_from_gtk_event_key ( + GTK_WIDGET (item->canvas), + key.keyval, key.string); + if (e_tep_event.key.string != NULL) { + e_tep_event.key.length = strlen (e_tep_event.key.string); + } else { + e_tep_event.key.length = 0; + } + + _get_tep (text); + ret = e_text_event_processor_handle_event (text->tep, &e_tep_event); + + if (event->type == GDK_KEY_PRESS) + g_signal_emit ( + text, e_text_signals[E_TEXT_KEYPRESS], 0, + e_tep_event.key.keyval, e_tep_event.key.state); + + if (e_tep_event.key.string) + g_free ((gpointer) e_tep_event.key.string); + + return ret; + } + break; + case GDK_BUTTON_PRESS: /* Fall Through */ + case GDK_BUTTON_RELEASE: + if ((!text->editing) + && text->editable + && (event->button.button == 1 || + event->button.button == 2)) { + e_canvas_item_grab_focus (item, TRUE); + start_editing (text); + } + + /* We follow convention and emit popup events on right-clicks. */ + if (event->type == GDK_BUTTON_PRESS && event->button.button == 3) { + if (text->handle_popup) { + e_text_do_popup ( + text, event, + get_position_from_xy ( + text, event->button.x, + event->button.y)); + return 1; + } + else { + break; + } + } + + /* Create our own double and triple click events, + * as gnome-canvas doesn't forward them to us */ + if (event->type == GDK_BUTTON_PRESS) { + if (text->dbl_timeout == 0 && + text->tpl_timeout == 0) { + text->dbl_timeout = g_timeout_add ( + 200, _click, &(text->dbl_timeout)); + } else { + if (text->tpl_timeout == 0) { + e_tep_event.type = GDK_2BUTTON_PRESS; + text->tpl_timeout = g_timeout_add ( + 200, _click, &(text->tpl_timeout)); + } else { + e_tep_event.type = GDK_3BUTTON_PRESS; + } + } + } + + if (text->editing) { + GdkEventButton button = event->button; + e_tep_event.button.time = button.time; + e_tep_event.button.state = button.state; + e_tep_event.button.button = button.button; + e_tep_event.button.position = + get_position_from_xy ( + text, button.x, button.y); + e_tep_event.button.device = + gdk_event_get_device (event); + _get_tep (text); + return_val = e_text_event_processor_handle_event ( + text->tep, &e_tep_event); + if (event->button.button == 1) { + if (event->type == GDK_BUTTON_PRESS) + text->button_down = TRUE; + else + text->button_down = FALSE; + } + text->lastx = button.x; + text->lasty = button.y; + text->last_state = button.state; + } + break; + case GDK_MOTION_NOTIFY: + if (text->editing) { + GdkEventMotion motion = event->motion; + e_tep_event.motion.time = motion.time; + e_tep_event.motion.state = motion.state; + e_tep_event.motion.position = + get_position_from_xy ( + text, motion.x, motion.y); + _get_tep (text); + return_val = e_text_event_processor_handle_event ( + text->tep, &e_tep_event); + text->lastx = motion.x; + text->lasty = motion.y; + text->last_state = motion.state; + } + break; + case GDK_ENTER_NOTIFY: + text->pointer_in = TRUE; + if (text->editing) { + if (text->default_cursor_shown) { + gdk_window_set_cursor (window, text->i_cursor); + text->default_cursor_shown = FALSE; + } + } + break; + case GDK_LEAVE_NOTIFY: + text->pointer_in = FALSE; + if (text->editing) { + if (!text->default_cursor_shown) { + gdk_window_set_cursor ( + window, text->default_cursor); + text->default_cursor_shown = TRUE; + } + } + break; + default: + break; + } + if (return_val) + return return_val; + if (GNOME_CANVAS_ITEM_CLASS (e_text_parent_class)->event) + return GNOME_CANVAS_ITEM_CLASS (e_text_parent_class)->event (item, event); + else + return 0; +} + +void +e_text_copy_clipboard (EText *text) +{ + gint selection_start_pos; + gint selection_end_pos; + + selection_start_pos = MIN (text->selection_start, text->selection_end); + selection_end_pos = MAX (text->selection_start, text->selection_end); + + /* convert sel_start/sel_end to byte indices */ + selection_start_pos = g_utf8_offset_to_pointer ( + text->text, selection_start_pos) - text->text; + selection_end_pos = g_utf8_offset_to_pointer ( + text->text, selection_end_pos) - text->text; + + gtk_clipboard_set_text ( + gtk_widget_get_clipboard ( + GTK_WIDGET (GNOME_CANVAS_ITEM (text)->canvas), + GDK_SELECTION_CLIPBOARD), + text->text + selection_start_pos, + selection_end_pos - selection_start_pos); +} + +void +e_text_delete_selection (EText *text) +{ + gint sel_start, sel_end; + + sel_start = MIN (text->selection_start, text->selection_end); + sel_end = MAX (text->selection_start, text->selection_end); + + if (sel_start != sel_end) + e_text_model_delete (text->model, sel_start, sel_end - sel_start); + text->need_im_reset = TRUE; +} + +void +e_text_cut_clipboard (EText *text) +{ + e_text_copy_clipboard (text); + e_text_delete_selection (text); +} + +void +e_text_paste_clipboard (EText *text) +{ + ETextEventProcessorCommand command; + + command.action = E_TEP_PASTE; + command.position = E_TEP_SELECTION; + command.string = ""; + command.value = 0; + e_text_command (text->tep, &command, text); +} + +void +e_text_select_all (EText *text) +{ + ETextEventProcessorCommand command; + + command.action = E_TEP_SELECT; + command.position = E_TEP_SELECT_ALL; + command.string = ""; + command.value = 0; + e_text_command (text->tep, &command, text); +} + +static void +primary_get_cb (GtkClipboard *clipboard, + GtkSelectionData *selection_data, + guint info, + gpointer data) +{ + EText *text = E_TEXT (data); + gint sel_start, sel_end; + + sel_start = MIN (text->selection_start, text->selection_end); + sel_end = MAX (text->selection_start, text->selection_end); + + /* convert sel_start/sel_end to byte indices */ + sel_start = g_utf8_offset_to_pointer (text->text, sel_start) - text->text; + sel_end = g_utf8_offset_to_pointer (text->text, sel_end) - text->text; + + if (sel_start != sel_end) { + gtk_selection_data_set_text ( + selection_data, + text->text + sel_start, + sel_end - sel_start); + } +} + +static void +primary_clear_cb (GtkClipboard *clipboard, + gpointer data) +{ +#ifdef notyet + /* XXX */ + gtk_editable_select_region ( + GTK_EDITABLE (entry), entry->current_pos, entry->current_pos); +#endif +} + +static void +e_text_update_primary_selection (EText *text) +{ + static const GtkTargetEntry targets[] = { + { (gchar *) "UTF8_STRING", 0, 0 }, + { (gchar *) "UTF-8", 0, 0 }, + { (gchar *) "STRING", 0, 0 }, + { (gchar *) "TEXT", 0, 0 }, + { (gchar *) "COMPOUND_TEXT", 0, 0 } + }; + GtkClipboard *clipboard; + + clipboard = gtk_widget_get_clipboard ( + GTK_WIDGET (GNOME_CANVAS_ITEM (text)->canvas), + GDK_SELECTION_PRIMARY); + + if (text->selection_start != text->selection_end) { + if (!gtk_clipboard_set_with_owner ( + clipboard, targets, G_N_ELEMENTS (targets), + primary_get_cb, primary_clear_cb, G_OBJECT (text))) + primary_clear_cb (clipboard, text); + } else { + if (gtk_clipboard_get_owner (clipboard) == G_OBJECT (text)) + gtk_clipboard_clear (clipboard); + } +} + +static void +paste_received (GtkClipboard *clipboard, + const gchar *text, + gpointer data) +{ + EText *etext = E_TEXT (data); + + if (text && g_utf8_validate (text, strlen (text), NULL)) { + if (etext->selection_end != etext->selection_start) + e_text_delete_selection (etext); + + e_text_insert (etext, text); + } + + g_object_unref (etext); +} + +static void +e_text_paste (EText *text, + GdkAtom selection) +{ + g_object_ref (text); + gtk_clipboard_request_text ( + gtk_widget_get_clipboard ( + GTK_WIDGET (GNOME_CANVAS_ITEM (text)->canvas), + selection), paste_received, text); +} + +typedef struct { + EText *text; + GdkEvent *event; + gint position; +} PopupClosure; + +static void +popup_menu_detach (GtkWidget *attach_widget, + GtkMenu *menu) +{ +} + +static void +popup_menu_placement_cb (GtkMenu *menu, + gint *x, + gint *y, + gboolean *push_in, + gpointer user_data) +{ + EText *text = E_TEXT (user_data); + GnomeCanvasItem *item = &text->item; + GnomeCanvas *parent = item->canvas; + + if (parent) { + GdkWindow *window; + + window = gtk_widget_get_window (GTK_WIDGET (parent)); + gdk_window_get_origin (window, x, y); + *x += item->x1 + text->width / 2; + *y += item->y1 + text->height / 2; + } + + return; +} + +static void +popup_targets_received (GtkClipboard *clipboard, + GtkSelectionData *data, + gpointer user_data) +{ + PopupClosure *closure = user_data; + EText *text = closure->text; + GdkEvent *event = closure->event; + gint position = closure->position; + GtkWidget *popup_menu = gtk_menu_new (); + GtkWidget *menuitem, *submenu; + guint event_button = 0; + guint32 event_time; + + gdk_event_get_button (event, &event_button); + event_time = gdk_event_get_time (event); + + g_free (closure); + + gtk_menu_attach_to_widget ( + GTK_MENU (popup_menu), + GTK_WIDGET (GNOME_CANVAS_ITEM (text)->canvas), + popup_menu_detach); + + /* cut menu item */ + menuitem = gtk_image_menu_item_new_from_stock (GTK_STOCK_CUT, NULL); + gtk_widget_show (menuitem); + gtk_menu_shell_append (GTK_MENU_SHELL (popup_menu), menuitem); + g_signal_connect_swapped ( + menuitem, "activate", + G_CALLBACK (e_text_cut_clipboard), text); + gtk_widget_set_sensitive ( + menuitem, text->editable && + (text->selection_start != text->selection_end)); + + /* copy menu item */ + menuitem = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL); + gtk_widget_show (menuitem); + gtk_menu_shell_append (GTK_MENU_SHELL (popup_menu), menuitem); + g_signal_connect_swapped ( + menuitem, "activate", + G_CALLBACK (e_text_copy_clipboard), text); + gtk_widget_set_sensitive (menuitem, text->selection_start != text->selection_end); + + /* paste menu item */ + menuitem = gtk_image_menu_item_new_from_stock (GTK_STOCK_PASTE, NULL); + gtk_widget_show (menuitem); + gtk_menu_shell_append (GTK_MENU_SHELL (popup_menu), menuitem); + g_signal_connect_swapped ( + menuitem, "activate", + G_CALLBACK (e_text_paste_clipboard), text); + gtk_widget_set_sensitive ( + menuitem, text->editable && + gtk_selection_data_targets_include_text (data)); + + menuitem = gtk_menu_item_new_with_label (_("Select All")); + gtk_widget_show (menuitem); + gtk_menu_shell_append (GTK_MENU_SHELL (popup_menu), menuitem); + g_signal_connect_swapped ( + menuitem, "activate", + G_CALLBACK (e_text_select_all), text); + gtk_widget_set_sensitive (menuitem, strlen (text->text) > 0); + + menuitem = gtk_separator_menu_item_new (); + gtk_widget_show (menuitem); + gtk_menu_shell_append (GTK_MENU_SHELL (popup_menu), menuitem); + + if (text->im_context && GTK_IS_IM_MULTICONTEXT (text->im_context)) { + menuitem = gtk_menu_item_new_with_label (_("Input Methods")); + gtk_widget_show (menuitem); + submenu = gtk_menu_new (); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (menuitem), submenu); + + gtk_menu_shell_append (GTK_MENU_SHELL (popup_menu), menuitem); + + gtk_im_multicontext_append_menuitems ( + GTK_IM_MULTICONTEXT (text->im_context), + GTK_MENU_SHELL (submenu)); + } + + g_signal_emit ( + text, + e_text_signals[E_TEXT_POPULATE_POPUP], + 0, + event, + position, + popup_menu); + + /* If invoked by S-F10 key binding, button will be 0. */ + if (event_button == 0) { + gtk_menu_popup ( + GTK_MENU (popup_menu), NULL, NULL, + popup_menu_placement_cb, (gpointer) text, + event_button, GDK_CURRENT_TIME); + } else { + gtk_menu_popup ( + GTK_MENU (popup_menu), NULL, NULL, + NULL, NULL, + event_button, event_time); + } + + g_object_unref (text); + gdk_event_free (event); +} + +static void +e_text_do_popup (EText *text, + GdkEvent *button_event, + gint position) +{ + PopupClosure *closure = g_new (PopupClosure, 1); + + closure->text = g_object_ref (text); + closure->event = gdk_event_copy (button_event); + closure->position = position; + + gtk_clipboard_request_contents ( + gtk_widget_get_clipboard ( + GTK_WIDGET (GNOME_CANVAS_ITEM (text)->canvas), + GDK_SELECTION_CLIPBOARD), + gdk_atom_intern ("TARGETS", FALSE), + popup_targets_received, + closure); +} + +static void +e_text_reset_im_context (EText *text) +{ + if (text->need_im_reset && text->im_context) { + text->need_im_reset = FALSE; + gtk_im_context_reset (text->im_context); + } +} + +/* fixme: */ + +static gint +next_word (EText *text, + gint start) +{ + gchar *p = g_utf8_offset_to_pointer (text->text, start); + gint length; + + length = g_utf8_strlen (text->text, -1); + + if (start >= length) { + return length; + } else { + p = g_utf8_next_char (p); + start++; + + while (p && *p) { + gunichar unival = g_utf8_get_char (p); + if (g_unichar_isspace (unival)) { + return start + 1; + } + else { + p = g_utf8_next_char (p); + start++; + } + } + } + + return g_utf8_pointer_to_offset (text->text, p); +} + +static gint +find_offset_into_line (EText *text, + gint offset_into_text, + gchar **start_of_line) +{ + gchar *p; + + p = g_utf8_offset_to_pointer (text->text, offset_into_text); + + if (p == text->text) { + if (start_of_line) + *start_of_line = (gchar *)text->text; + return 0; + } + else { + p = g_utf8_find_prev_char (text->text, p); + + while (p && p > text->text) { + if (*p == '\n') { + if (start_of_line) + *start_of_line = p+1; + return offset_into_text - + g_utf8_pointer_to_offset ( + text->text, p + 1); + } + p = g_utf8_find_prev_char (text->text, p); + } + + if (start_of_line) + *start_of_line = (gchar *)text->text; + return offset_into_text; + } +} + +/* direction = TRUE (move forward), FALSE (move backward) + * Any error shall return length (text->text) or 0 or + * text->selection_end (as deemed fit) */ +static gint +_get_updated_position (EText *text, + gboolean direction) +{ + PangoLogAttr *log_attrs = NULL; + gint n_attrs; + gchar *p = NULL; + gint new_pos = 0; + gint length = 0; + + /* Basic sanity test, return whatever position we are currently at. */ + g_return_val_if_fail (text->layout != NULL, text->selection_end); + + length = g_utf8_strlen (text->text, -1); + + /* length checks to make sure we are not wandering + * off into nonexistant memory... */ + if ((text->selection_end >= length) && (TRUE == direction)) /* forward */ + return length; + /* checking for -ve value wont hurt! */ + if ((text->selection_end <= 0) && (FALSE == direction)) /* backward */ + return 0; + + /* check for validness of full text->text */ + if (!g_utf8_validate (text->text, -1, NULL)) + return text->selection_end; + + /* get layout's PangoLogAttr to facilitate moving when + * moving across grapheme cluster as in indic langs */ + pango_layout_get_log_attrs (text->layout, &log_attrs, &n_attrs); + + /* Fetch the current gchar index in the line & keep moving + * forward until we can display cursor */ + p = g_utf8_offset_to_pointer (text->text, text->selection_end); + + new_pos = text->selection_end; + while (1) + { + /* check before moving forward/backwards + * if we have more chars to move or not */ + if (TRUE == direction) + p = g_utf8_next_char (p); + else + p = g_utf8_prev_char (p); + + /* validate the new string & return with original position if check fails */ + if (!g_utf8_validate (p, -1, NULL)) + break; /* will return old value of new_pos */ + + new_pos = g_utf8_pointer_to_offset (text->text, p); + + /* if is_cursor_position is set, cursor can appear in front of character. + * i.e. this is a grapheme boundary AND make some sanity checks */ + if ((new_pos >=0) && (new_pos < n_attrs) && + (log_attrs[new_pos].is_cursor_position)) + break; + else if ((new_pos < 0) || (new_pos >= n_attrs)) + { + new_pos = text->selection_end; + break; + } + } + + if (log_attrs) + g_free (log_attrs); + + return new_pos; +} + +static gint +_get_position (EText *text, + ETextEventProcessorCommand *command) +{ + gint length, obj_num; + gunichar unival; + gchar *p = NULL; + gint new_pos = 0; + + switch (command->position) { + + case E_TEP_VALUE: + new_pos = command->value; + break; + + case E_TEP_SELECTION: + new_pos = text->selection_end; + break; + + case E_TEP_START_OF_BUFFER: + new_pos = 0; + break; + + case E_TEP_END_OF_BUFFER: + new_pos = strlen (text->text); + break; + + case E_TEP_START_OF_LINE: + + if (text->selection_end >= 1) { + + p = g_utf8_offset_to_pointer (text->text, text->selection_end); + if (p != text->text) { + p = g_utf8_find_prev_char (text->text, p); + while (p && p > text->text) { + if (*p == '\n') { + new_pos = g_utf8_pointer_to_offset (text->text, p) + 1; + break; + } + p = g_utf8_find_prev_char (text->text, p); + } + } + } + + break; + + case E_TEP_END_OF_LINE: + new_pos = -1; + length = g_utf8_strlen (text->text, -1); + + if (text->selection_end >= length) { + new_pos = length; + } else { + + p = g_utf8_offset_to_pointer (text->text, text->selection_end); + + while (p && *p) { + if (*p == '\n') { + new_pos = g_utf8_pointer_to_offset (text->text, p); + p = NULL; + } else + p = g_utf8_next_char (p); + } + } + + if (new_pos == -1) + new_pos = g_utf8_pointer_to_offset (text->text, p); + + break; + + case E_TEP_FORWARD_CHARACTER: + length = g_utf8_strlen (text->text, -1); + + if (text->selection_end >= length) + new_pos = length; + else + /* get updated position to display cursor */ + new_pos = _get_updated_position (text, TRUE); + + break; + + case E_TEP_BACKWARD_CHARACTER: + new_pos = 0; + if (text->selection_end >= 1) + /* get updated position to display cursor */ + new_pos = _get_updated_position (text, FALSE); + + break; + + case E_TEP_FORWARD_WORD: + new_pos = next_word (text, text->selection_end); + break; + + case E_TEP_BACKWARD_WORD: + new_pos = 0; + if (text->selection_end >= 1) { + gint pos = text->selection_end; + + p = g_utf8_find_prev_char ( + text->text, g_utf8_offset_to_pointer ( + text->text, text->selection_end)); + pos--; + + if (p != text->text) { + p = g_utf8_find_prev_char (text->text, p); + pos--; + + while (p && p > text->text) { + unival = g_utf8_get_char (p); + if (g_unichar_isspace (unival)) { + new_pos = pos + 1; + p = NULL; + } + else { + p = g_utf8_find_prev_char (text->text, p); + pos--; + } + } + } + } + + break; + + case E_TEP_FORWARD_LINE: { + gint offset_into_line; + + offset_into_line = find_offset_into_line (text, text->selection_end, NULL); + if (offset_into_line == -1) + return text->selection_end; + + /* now we search forward til we hit a \n, and then + * offset_into_line more characters */ + p = g_utf8_offset_to_pointer (text->text, text->selection_end); + while (p && *p) { + if (*p == '\n') + break; + p = g_utf8_next_char (p); + } + if (p && *p == '\n') { + /* now we loop forward offset_into_line + * characters, or until we hit \n or \0 */ + + p = g_utf8_next_char (p); + while (offset_into_line > 0 && p && *p != '\n' && *p != '\0') { + p = g_utf8_next_char (p); + offset_into_line--; + } + } + + /* at this point, p points to the new location, + * convert it to an offset and we're done */ + new_pos = g_utf8_pointer_to_offset (text->text, p); + break; + } + case E_TEP_BACKWARD_LINE: { + gint offset_into_line; + + offset_into_line = find_offset_into_line ( + text, text->selection_end, &p); + + if (offset_into_line == -1) + return text->selection_end; + + /* p points to the first character on our line. if we + * have a \n before it, skip it and scan til we hit + * the next one */ + if (p != text->text) { + p = g_utf8_find_prev_char (text->text, p); + if (*p == '\n') { + p = g_utf8_find_prev_char (text->text, p); + while (p > text->text) { + if (*p == '\n') { + p++; + break; + } + p = g_utf8_find_prev_char (text->text, p); + } + } + } + + /* at this point 'p' points to the start of the + * previous line, move forward 'offset_into_line' + * times. */ + + while (offset_into_line > 0 && p && *p != '\n' && *p != '\0') { + p = g_utf8_next_char (p); + offset_into_line--; + } + + /* at this point, p points to the new location, + * convert it to an offset and we're done */ + new_pos = g_utf8_pointer_to_offset (text->text, p); + break; + } + case E_TEP_SELECT_WORD: + /* This is a silly hack to cause double-clicking on an object + * to activate that object. + * (Normally, double click == select word, which is why this is here.) */ + + obj_num = e_text_model_get_object_at_offset ( + text->model, text->selection_start); + if (obj_num != -1) { + e_text_model_activate_nth_object (text->model, obj_num); + new_pos = text->selection_start; + break; + } + + if (text->selection_end < 1) { + new_pos = 0; + break; + } + + p = g_utf8_offset_to_pointer (text->text, text->selection_end); + + p = g_utf8_find_prev_char (text->text, p); + + while (p && p > text->text) { + unival = g_utf8_get_char (p); + if (g_unichar_isspace (unival)) { + p = g_utf8_next_char (p); + break; + } + p = g_utf8_find_prev_char (text->text, p); + } + + if (!p) + text->selection_start = 0; + else + text->selection_start = g_utf8_pointer_to_offset (text->text, p); + + text->selection_start = + e_text_model_validate_position ( + text->model, text->selection_start); + + length = g_utf8_strlen (text->text, -1); + if (text->selection_end >= length) { + new_pos = length; + break; + } + + p = g_utf8_offset_to_pointer (text->text, text->selection_end); + while (p && *p) { + unival = g_utf8_get_char (p); + if (g_unichar_isspace (unival)) { + new_pos = g_utf8_pointer_to_offset (text->text, p); + break; + } else + p = g_utf8_next_char (p); + } + + if (!new_pos) + new_pos = g_utf8_strlen (text->text, -1); + + return new_pos; + + case E_TEP_SELECT_ALL: + text->selection_start = 0; + new_pos = g_utf8_strlen (text->text, -1); + break; + + case E_TEP_FORWARD_PARAGRAPH: + case E_TEP_BACKWARD_PARAGRAPH: + + case E_TEP_FORWARD_PAGE: + case E_TEP_BACKWARD_PAGE: + new_pos = text->selection_end; + break; + + default: + new_pos = text->selection_end; + break; + } + + new_pos = e_text_model_validate_position (text->model, new_pos); + + return new_pos; +} + +static void +e_text_insert (EText *text, + const gchar *string) +{ + gint len = strlen (string); + + if (len > 0) { + gint utf8len = 0; + + if (!text->allow_newlines) { + const gchar *i; + gchar *new_string = g_malloc (len + 1); + gchar *j = new_string; + + for (i = string; *i; i = g_utf8_next_char (i)) { + if (*i != '\n') { + gunichar c; + gint charlen; + + c = g_utf8_get_char (i); + charlen = g_unichar_to_utf8 (c, j); + j += charlen; + utf8len++; + } + } + *j = 0; + e_text_model_insert_length ( + text->model, text->selection_start, + new_string, utf8len); + g_free (new_string); + } + else { + utf8len = g_utf8_strlen (string, -1); + e_text_model_insert_length ( + text->model, text->selection_start, + string, utf8len); + } + } +} + +static void +capitalize (EText *text, + gint start, + gint end, + ETextEventProcessorCaps type) +{ + gboolean first = TRUE; + const gchar *p = g_utf8_offset_to_pointer (text->text, start); + const gchar *text_end = g_utf8_offset_to_pointer (text->text, end); + gint utf8len = text_end - p; + + if (utf8len > 0) { + gchar *new_text = g_new0 (char, utf8len * 6); + gchar *output = new_text; + + while (p && *p && p < text_end) { + gunichar unival = g_utf8_get_char (p); + gunichar newval = unival; + + switch (type) { + case E_TEP_CAPS_UPPER: + newval = g_unichar_toupper (unival); + break; + case E_TEP_CAPS_LOWER: + newval = g_unichar_tolower (unival); + break; + case E_TEP_CAPS_TITLE: + if (g_unichar_isalpha (unival)) { + if (first) + newval = g_unichar_totitle (unival); + else + newval = g_unichar_tolower (unival); + first = FALSE; + } else { + first = TRUE; + } + break; + } + g_unichar_to_utf8 (newval, output); + output = g_utf8_next_char (output); + + p = g_utf8_next_char (p); + } + *output = 0; + + e_text_model_delete (text->model, start, utf8len); + e_text_model_insert_length (text->model, start, new_text, utf8len); + g_free (new_text); + } +} + +static void +e_text_command (ETextEventProcessor *tep, + ETextEventProcessorCommand *command, + gpointer data) +{ + EText *text = E_TEXT (data); + gboolean scroll = TRUE; + gboolean use_start = TRUE; + + switch (command->action) { + case E_TEP_MOVE: + text->selection_start = _get_position (text, command); + text->selection_end = text->selection_start; + if (text->timer) { + g_timer_reset (text->timer); + } + + text->need_im_reset = TRUE; + use_start = TRUE; + break; + case E_TEP_SELECT: + text->selection_start = + e_text_model_validate_position ( + text->model, text->selection_start); /* paranoia */ + text->selection_end = _get_position (text, command); + + e_text_update_primary_selection (text); + + text->need_im_reset = TRUE; + use_start = FALSE; + + break; + case E_TEP_DELETE: + if (text->selection_end == text->selection_start) { + text->selection_end = _get_position (text, command); + } + e_text_delete_selection (text); + if (text->timer) { + g_timer_reset (text->timer); + } + + text->need_im_reset = TRUE; + use_start = FALSE; + + break; + + case E_TEP_INSERT: + if (g_utf8_validate (command->string, command->value, NULL)) { + if (text->selection_end != text->selection_start) { + e_text_delete_selection (text); + } + e_text_insert (text, command->string); + if (text->timer) { + g_timer_reset (text->timer); + } + text->need_im_reset = TRUE; + } + break; + case E_TEP_COPY: + e_text_copy_clipboard (text); + + if (text->timer) { + g_timer_reset (text->timer); + } + scroll = FALSE; + break; + case E_TEP_PASTE: + e_text_paste (text, GDK_NONE); + if (text->timer) { + g_timer_reset (text->timer); + } + text->need_im_reset = TRUE; + break; + case E_TEP_GET_SELECTION: + e_text_paste (text, GDK_SELECTION_PRIMARY); + break; + case E_TEP_ACTIVATE: + g_signal_emit (text, e_text_signals[E_TEXT_ACTIVATE], 0); + if (text->timer) { + g_timer_reset (text->timer); + } + break; + case E_TEP_SET_SELECT_BY_WORD: + text->select_by_word = command->value; + break; + case E_TEP_GRAB: + e_canvas_item_grab ( + E_CANVAS (GNOME_CANVAS_ITEM (text)->canvas), + GNOME_CANVAS_ITEM (text), + GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK, + text->i_cursor, + command->device, + command->time, + NULL, + NULL); + scroll = FALSE; + break; + case E_TEP_UNGRAB: + e_canvas_item_ungrab ( + E_CANVAS (GNOME_CANVAS_ITEM (text)->canvas), + GNOME_CANVAS_ITEM (text), + command->time); + scroll = FALSE; + break; + case E_TEP_CAPS: + if (text->selection_start == text->selection_end) { + capitalize ( + text, text->selection_start, + next_word (text, text->selection_start), + command->value); + } else { + gint selection_start = MIN ( + text->selection_start, text->selection_end); + gint selection_end = MAX ( + text->selection_start, text->selection_end); + capitalize ( + text, selection_start, + selection_end, command->value); + } + break; + case E_TEP_NOP: + scroll = FALSE; + break; + } + + e_text_reset_im_context (text); + + /* it's possible to get here without ever having been realized + * by our canvas (if the e-text started completely obscured.) + * so let's create our layout object if we don't already have + * one. */ + if (!text->layout) + create_layout (text); + + /* We move cursor only if scroll is TRUE */ + if (scroll && !text->button_down) { + /* XXX do we really need the @trailing logic here? if + * we don't we can scrap the loop and just use + * pango_layout_index_to_pos */ + PangoLayoutLine *cur_line = NULL; + gint selection_index; + PangoLayoutIter *iter = pango_layout_get_iter (text->layout); + + /* check if we are using selection_start or selection_end for moving? */ + selection_index = use_start ? text->selection_start : text->selection_end; + + /* convert to a byte index */ + selection_index = g_utf8_offset_to_pointer ( + text->text, selection_index) - text->text; + + do { + PangoLayoutLine *line = pango_layout_iter_get_line (iter); + + if (selection_index >= line->start_index && + selection_index <= line->start_index + line->length) { + /* found the line with the start of the selection */ + cur_line = line; + break; + } + + } while (pango_layout_iter_next_line (iter)); + + if (cur_line) { + gint xpos, ypos; + gdouble clip_width, clip_height; + /* gboolean trailing = FALSE; */ + PangoRectangle pango_pos; + + if (selection_index > 0 && selection_index == + cur_line->start_index + cur_line->length) { + selection_index--; + /* trailing = TRUE; */ + } + + pango_layout_index_to_pos (text->layout, selection_index, &pango_pos); + + pango_pos.x = PANGO_PIXELS (pango_pos.x); + pango_pos.y = PANGO_PIXELS (pango_pos.y); + pango_pos.width = (pango_pos.width + PANGO_SCALE / 2) / PANGO_SCALE; + pango_pos.height = (pango_pos.height + PANGO_SCALE / 2) / PANGO_SCALE; + + /* scroll for X */ + xpos = pango_pos.x; /* + (trailing ? 0 : pango_pos.width);*/ + + if (xpos + 2 < text->xofs_edit) { + text->xofs_edit = xpos; + } + + clip_width = text->clip_width; + + if (xpos + pango_pos.width - clip_width > text->xofs_edit) { + text->xofs_edit = xpos + pango_pos.width - clip_width; + } + + /* scroll for Y */ + if (pango_pos.y + 2 < text->yofs_edit) { + ypos = pango_pos.y; + text->yofs_edit = ypos; + } + else { + ypos = pango_pos.y + pango_pos.height; + } + + if (text->clip_height < 0) + clip_height = text->height; + else + clip_height = text->clip_height; + + if (ypos - clip_height > text->yofs_edit) { + text->yofs_edit = ypos - clip_height; + } + + } + + pango_layout_iter_free (iter); + } + + text->needs_redraw = 1; + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (text)); +} + +/* Class initialization function for the text item */ +static void +e_text_class_init (ETextClass *class) +{ + GObjectClass *gobject_class; + GnomeCanvasItemClass *item_class; + + gobject_class = (GObjectClass *) class; + item_class = (GnomeCanvasItemClass *) class; + + gobject_class->dispose = e_text_dispose; + gobject_class->set_property = e_text_set_property; + gobject_class->get_property = e_text_get_property; + + item_class->update = e_text_update; + item_class->realize = e_text_realize; + item_class->unrealize = e_text_unrealize; + item_class->draw = e_text_draw; + item_class->point = e_text_point; + item_class->bounds = e_text_bounds; + item_class->event = e_text_event; + + class->changed = NULL; + class->activate = NULL; + + e_text_signals[E_TEXT_CHANGED] = g_signal_new ( + "changed", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETextClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + e_text_signals[E_TEXT_ACTIVATE] = g_signal_new ( + "activate", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETextClass, activate), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + e_text_signals[E_TEXT_KEYPRESS] = g_signal_new ( + "keypress", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETextClass, keypress), + NULL, NULL, + e_marshal_NONE__INT_INT, + G_TYPE_NONE, 2, + G_TYPE_UINT, + G_TYPE_UINT); + + e_text_signals[E_TEXT_POPULATE_POPUP] = g_signal_new ( + "populate_popup", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETextClass, populate_popup), + NULL, NULL, + e_marshal_NONE__POINTER_INT_OBJECT, + G_TYPE_NONE, 3, + G_TYPE_POINTER, + G_TYPE_INT, + GTK_TYPE_MENU); + + g_object_class_install_property ( + gobject_class, + PROP_MODEL, + g_param_spec_object ( + "model", + "Model", + "Model", + E_TYPE_TEXT_MODEL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_EVENT_PROCESSOR, + g_param_spec_object ( + "event_processor", + "Event Processor", + "Event Processor", + E_TEXT_EVENT_PROCESSOR_TYPE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_TEXT, + g_param_spec_string ( + "text", + "Text", + "Text", + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_BOLD, + g_param_spec_boolean ( + "bold", + "Bold", + "Bold", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_STRIKEOUT, + g_param_spec_boolean ( + "strikeout", + "Strikeout", + "Strikeout", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_JUSTIFICATION, + g_param_spec_enum ( + "justification", + "Justification", + "Justification", + GTK_TYPE_JUSTIFICATION, + GTK_JUSTIFY_LEFT, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_CLIP_WIDTH, + g_param_spec_double ( + "clip_width", + "Clip Width", + "Clip Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_CLIP_HEIGHT, + g_param_spec_double ( + "clip_height", + "Clip Height", + "Clip Height", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_CLIP, + g_param_spec_boolean ( + "clip", + "Clip", + "Clip", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_FILL_CLIP_RECTANGLE, + g_param_spec_boolean ( + "fill_clip_rectangle", + "Fill clip rectangle", + "Fill clip rectangle", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_X_OFFSET, + g_param_spec_double ( + "x_offset", + "X Offset", + "X Offset", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_Y_OFFSET, + g_param_spec_double ( + "y_offset", + "Y Offset", + "Y Offset", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_FILL_COLOR, + g_param_spec_string ( + "fill_color", + "Fill color", + "Fill color", + NULL, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + gobject_class, + PROP_FILL_COLOR_GDK, + g_param_spec_boxed ( + "fill_color_gdk", + "GDK fill color", + "GDK fill color", + GDK_TYPE_COLOR, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + gobject_class, + PROP_FILL_COLOR_RGBA, + g_param_spec_uint ( + "fill_color_rgba", + "GDK fill color", + "GDK fill color", + 0, G_MAXUINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_TEXT_WIDTH, + g_param_spec_double ( + "text_width", + "Text width", + "Text width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + gobject_class, + PROP_TEXT_HEIGHT, + g_param_spec_double ( + "text_height", + "Text height", + "Text height", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READABLE)); + + g_object_class_install_property ( + gobject_class, + PROP_EDITABLE, + g_param_spec_boolean ( + "editable", + "Editable", + "Editable", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_USE_ELLIPSIS, + g_param_spec_boolean ( + "use_ellipsis", + "Use ellipsis", + "Use ellipsis", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_ELLIPSIS, + g_param_spec_string ( + "ellipsis", + "Ellipsis", + "Ellipsis", + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_LINE_WRAP, + g_param_spec_boolean ( + "line_wrap", + "Line wrap", + "Line wrap", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_BREAK_CHARACTERS, + g_param_spec_string ( + "break_characters", + "Break characters", + "Break characters", + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, PROP_MAX_LINES, + g_param_spec_int ( + "max_lines", + "Max lines", + "Max lines", + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_WIDTH, + g_param_spec_double ( + "width", + "Width", + "Width", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_HEIGHT, + g_param_spec_double ( + "height", + "Height", + "Height", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_ALLOW_NEWLINES, + g_param_spec_boolean ( + "allow_newlines", + "Allow newlines", + "Allow newlines", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_CURSOR_POS, + g_param_spec_int ( + "cursor_pos", + "Cursor position", + "Cursor position", + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_IM_CONTEXT, + g_param_spec_object ( + "im_context", + "IM Context", + "IM Context", + GTK_TYPE_IM_CONTEXT, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + gobject_class, + PROP_HANDLE_POPUP, + g_param_spec_boolean ( + "handle_popup", + "Handle Popup", + "Handle Popup", + FALSE, + G_PARAM_READWRITE)); + + if (!clipboard_atom) + clipboard_atom = gdk_atom_intern ("CLIPBOARD", FALSE); + + gal_a11y_e_text_init (); +} + +/* Object initialization function for the text item */ +static void +e_text_init (EText *text) +{ + text->model = e_text_model_new (); + text->text = e_text_model_get_text (text->model); + text->preedit_len = 0; + text->preedit_pos = 0; + text->layout = NULL; + + text->revert = NULL; + + text->model_changed_signal_id = g_signal_connect ( + text->model, "changed", + G_CALLBACK (e_text_text_model_changed), text); + + text->model_repos_signal_id = g_signal_connect ( + text->model, "reposition", + G_CALLBACK (e_text_text_model_reposition), text); + + text->justification = GTK_JUSTIFY_LEFT; + text->clip_width = -1.0; + text->clip_height = -1.0; + text->xofs = 0.0; + text->yofs = 0.0; + + text->ellipsis = NULL; + text->use_ellipsis = FALSE; + text->ellipsis_width = 0; + + text->editable = FALSE; + text->editing = FALSE; + text->xofs_edit = 0; + text->yofs_edit = 0; + + text->selection_start = 0; + text->selection_end = 0; + text->select_by_word = FALSE; + + text->timeout_id = 0; + text->timer = NULL; + + text->lastx = 0; + text->lasty = 0; + text->last_state = 0; + + text->scroll_start = 0; + text->show_cursor = TRUE; + text->button_down = FALSE; + + text->tep = NULL; + text->tep_command_id = 0; + + text->pointer_in = FALSE; + text->default_cursor_shown = TRUE; + text->line_wrap = FALSE; + text->break_characters = NULL; + text->max_lines = -1; + text->dbl_timeout = 0; + text->tpl_timeout = 0; + + text->bold = FALSE; + text->strikeout = FALSE; + + text->allow_newlines = TRUE; + + text->last_type_request = -1; + text->last_time_request = 0; + text->queued_requests = NULL; + + text->im_context = NULL; + text->need_im_reset = FALSE; + text->im_context_signals_registered = FALSE; + + text->handle_popup = FALSE; + text->rgba_set = FALSE; + + e_canvas_item_set_reflow_callback (GNOME_CANVAS_ITEM (text), e_text_reflow); +} + +/* IM Context Callbacks */ +static void +e_text_commit_cb (GtkIMContext *context, + const gchar *str, + EText *text) +{ + if (g_utf8_validate (str, strlen (str), NULL)) { + if (text->selection_end != text->selection_start) + e_text_delete_selection (text); + e_text_insert (text, str); + g_signal_emit (text, e_text_signals[E_TEXT_KEYPRESS], 0, 0, 0); + } +} + +static void +e_text_preedit_changed_cb (GtkIMContext *context, + EText *etext) +{ + gchar *preedit_string = NULL; + gint cursor_pos; + + gtk_im_context_get_preedit_string ( + context, &preedit_string, + NULL, &cursor_pos); + + cursor_pos = CLAMP (cursor_pos, 0, g_utf8_strlen (preedit_string, -1)); + etext->preedit_len = strlen (preedit_string); + etext->preedit_pos = g_utf8_offset_to_pointer ( + preedit_string, cursor_pos) - preedit_string; + g_free (preedit_string); + + g_signal_emit (etext, e_text_signals[E_TEXT_KEYPRESS], 0, 0, 0); +} + +static gboolean +e_text_retrieve_surrounding_cb (GtkIMContext *context, + EText *text) +{ + gtk_im_context_set_surrounding ( + context, text->text, strlen (text->text), + g_utf8_offset_to_pointer (text->text, MIN ( + text->selection_start, text->selection_end)) - text->text); + + return TRUE; +} + +static gboolean +e_text_delete_surrounding_cb (GtkIMContext *context, + gint offset, + gint n_chars, + EText *text) +{ + e_text_model_delete ( + text->model, + MIN (text->selection_start, text->selection_end) + offset, + n_chars); + + return TRUE; +} diff --git a/e-util/e-text.h b/e-util/e-text.h new file mode 100644 index 0000000000..40e92bf585 --- /dev/null +++ b/e-util/e-text.h @@ -0,0 +1,236 @@ +/* + * e-text.h - Text item for evolution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Jon Trowbridge <trow@ximian.com> + * + * A majority of code taken from: + * + * Text item type for GnomeCanvas widget + * + * GnomeCanvas is basically a port of the Tk toolkit's most excellent + * canvas widget. Tk is copyrighted by the Regents of the University + * of California, Sun Microsystems, and other parties. + * + * Copyright (C) 1998 The Free Software Foundation + * + * Author: Federico Mena <federico@nuclecu.unam.mx> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_TEXT_H +#define E_TEXT_H + +#include <gtk/gtk.h> + +#include <e-util/e-canvas.h> +#include <e-util/e-text-event-processor.h> +#include <e-util/e-text-model.h> + +G_BEGIN_DECLS + +/* Text item for the canvas. Text items are positioned by an anchor point and an anchor direction. + * + * A clipping rectangle may be specified for the text. The rectangle is anchored at the text's anchor + * point, and is specified by clipping width and height parameters. If the clipping rectangle is + * enabled, it will clip the text. + * + * In addition, x and y offset values may be specified. These specify an offset from the anchor + * position. If used in conjunction with the clipping rectangle, these could be used to implement + * simple scrolling of the text within the clipping rectangle. + * + * The following object arguments are available: + * + * name type read/write description + * ------------------------------------------------------------------------------------------ + * text string RW The string of the text label + * bold boolean RW Bold? + * justification GtkJustification RW Justification for multiline text + * fill_color string W X color specification for text + * fill_color_gdk GdkColor* RW Pointer to an allocated GdkColor + * clip_width gdouble RW Width of clip rectangle + * clip_height gdouble RW Height of clip rectangle + * clip boolean RW Use clipping rectangle? + * fill_clip_rect boolean RW Whether the text item represents itself as being the size of the clipping rectangle. + * x_offset gdouble RW Horizontal offset distance from anchor position + * y_offset gdouble RW Vertical offset distance from anchor position + * text_width gdouble R Used to query the width of the rendered text + * text_height gdouble R Used to query the rendered height of the text + * width gdouble RW A synonym for clip_width + * height gdouble R A synonym for text_height + * + * These are currently ignored in the AA version: + * editable boolean RW Can this item be edited + * use_ellipsis boolean RW Whether to use ellipsises if text gets cut off. Meaningless if clip == false. + * ellipsis string RW The characters to use as ellipsis. NULL = "...". + * line_wrap boolean RW Line wrap when not editing. + * break_characters string RW List of characters to optionally break on. + * max_lines gint RW Number of lines possible when doing line wrap. + */ + +#define E_TYPE_TEXT (e_text_get_type ()) +#define E_TEXT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_TYPE_TEXT, EText)) +#define E_TEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), E_TYPE_TEXT, ETextClass)) +#define E_IS_TEXT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_TEXT)) +#define E_IS_TEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), E_TYPE_TEXT)) + +typedef struct _EText EText; +typedef struct _ETextClass ETextClass; + +struct _EText { + GnomeCanvasItem item; + + ETextModel *model; + gint model_changed_signal_id; + gint model_repos_signal_id; + + const gchar *text; /* Text to display --- from the ETextModel */ + gint preedit_len; /* preedit length to display */ + gint preedit_pos; /* preedit cursor position */ + PangoLayout *layout; + gint num_lines; /* Number of lines of text */ + + gchar *revert; /* Text to revert to */ + + GtkJustification justification; /* Justification for text */ + + gdouble clip_width; /* Width of optional clip rectangle */ + gdouble clip_height; /* Height of optional clip rectangle */ + + gdouble xofs, yofs; /* Text offset distance from anchor position */ + + gint cx, cy; /* Top-left canvas coordinates for text */ + gint text_cx, text_cy; /* Top-left canvas coordinates for text */ + gint clip_cx, clip_cy; /* Top-left canvas coordinates for clip rectangle */ + gint clip_cwidth, clip_cheight; /* Size of clip rectangle in pixels */ + gint max_width; /* Maximum width of text lines */ + gint width; /* Rendered text width in pixels */ + gint height; /* Rendered text height in pixels */ + + guint32 rgba; /* RGBA color for text */ + gboolean rgba_set; /* whether RGBA is set */ + + gchar *ellipsis; /* The ellipsis characters. NULL = "...". */ + gdouble ellipsis_width; /* The width of the ellipsis. */ + + gint xofs_edit; /* Offset because of editing */ + gint yofs_edit; /* Offset because of editing */ + + /* This needs to be reworked a bit once we get line wrapping. */ + gint selection_start; /* Start of selection IN BYTES */ + gint selection_end; /* End of selection IN BYTES */ + gboolean select_by_word; /* Current selection is by word */ + + /* This section is for drag scrolling and blinking cursor. */ + gint timeout_id; /* Current timeout id for scrolling */ + GTimer *timer; /* Timer for blinking cursor and scrolling */ + + gint lastx, lasty; /* Last x and y motion events */ + gint last_state; /* Last state */ + gulong scroll_start; /* Starting time for scroll (microseconds) */ + + gint show_cursor; /* Is cursor currently shown */ + gboolean button_down; /* Is mouse button 1 down */ + + ETextEventProcessor *tep; /* Text Event Processor */ + gint tep_command_id; + + gboolean has_selection; /* TRUE if we have the selection */ + + guint clip : 1; /* Use clip rectangle? */ + guint fill_clip_rectangle : 1; /* Fill the clipping rectangle. */ + + guint pointer_in : 1; /* Is the pointer currently over us? */ + guint default_cursor_shown : 1; /* Is the default cursor currently shown? */ + + guint line_wrap : 1; /* Do line wrap */ + + guint needs_redraw : 1; /* Needs redraw */ + guint needs_recalc_bounds : 1; /* Need recalc_bounds */ + guint needs_calc_height : 1; /* Need calc_height */ + guint needs_split_into_lines : 1; /* Needs split_into_lines */ + guint needs_reset_layout : 1; /* Needs split_into_lines */ + + guint bold : 1; + guint strikeout : 1; + + guint tooltip_owner : 1; + guint allow_newlines : 1; + + guint use_ellipsis : 1; /* Whether to use the ellipsis. */ + + guint editable : 1; /* Item is editable */ + guint editing : 1; /* Item is currently being edited */ + + gchar *break_characters; /* Characters to optionally break after */ + + gint max_lines; /* Max number of lines (-1 = infinite) */ + + GdkCursor *default_cursor; /* Default cursor (arrow) */ + GdkCursor *i_cursor; /* I beam cursor */ + + gint tooltip_timeout; /* Timeout for the tooltip */ + gint tooltip_count; /* GDK_ENTER_NOTIFY count. */ + + gint dbl_timeout; /* Double click timeout */ + gint tpl_timeout; /* Triple click timeout */ + + gint last_type_request; /* Last selection type requested. */ + guint32 last_time_request; /* The time of the last selection request. */ + GdkAtom last_selection_request; /* The time of the last selection request. */ + GList *queued_requests; /* Queued selection requests. */ + + GtkIMContext *im_context; + gboolean need_im_reset; + gboolean im_context_signals_registered; + + gboolean handle_popup; + + PangoFontDescription *font_desc; +}; + +struct _ETextClass { + GnomeCanvasItemClass parent_class; + + void (* changed) (EText *text); + void (* activate) (EText *text); + void (* keypress) (EText *text, guint keyval, guint state); + void (* populate_popup) (EText *text, GdkEvent *button_event, gint pos, GtkMenu *menu); + void (* style_set) (EText *text, GtkStyle *previous_style); +}; + +/* Standard Gtk function */ +GType e_text_get_type (void); +void e_text_cancel_editing (EText *text); +void e_text_stop_editing (EText *text); + +void e_text_delete_selection (EText *text); +void e_text_cut_clipboard (EText *text); +void e_text_copy_clipboard (EText *text); +void e_text_paste_clipboard (EText *text); +void e_text_select_all (EText *text); + +G_END_DECLS + +#endif diff --git a/e-util/e-timezone-dialog.c b/e-util/e-timezone-dialog.c new file mode 100644 index 0000000000..431287c2df --- /dev/null +++ b/e-util/e-timezone-dialog.c @@ -0,0 +1,870 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-timezone-dialog.h" + +#include <time.h> +#include <string.h> +#include <glib/gi18n.h> + +#include <libecal/libecal.h> + +#include "e-map.h" +#include "e-misc-utils.h" +#include "e-util-private.h" + +#ifdef G_OS_WIN32 +#ifdef gmtime_r +#undef gmtime_r +#endif +#ifdef localtime_r +#undef localtime_r +#endif + +/* The gmtime() and localtime() in Microsoft's C library are MT-safe */ +#define gmtime_r(tp,tmp) (gmtime(tp)?(*(tmp)=*gmtime(tp),(tmp)):0) +#define localtime_r(tp,tmp) (localtime(tp)?(*(tmp)=*localtime(tp),(tmp)):0) +#endif + +#define E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA 0xc070a0ff +#define E_TIMEZONE_DIALOG_MAP_POINT_HOVER_RGBA 0xffff60ff +#define E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_1_RGBA 0xff60e0ff +#define E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_2_RGBA 0x000000ff + +#define E_TIMEZONE_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TIMEZONE_DIALOG, ETimezoneDialogPrivate)) + +struct _ETimezoneDialogPrivate { + /* The selected timezone. May be NULL for a 'local time' (i.e. when + * the displayed name is ""). */ + icaltimezone *zone; + + GtkBuilder *builder; + + EMapPoint *point_selected; + EMapPoint *point_hover; + + EMap *map; + + /* The timeout used to flash the nearest point. */ + guint timeout_id; + + /* Widgets from the UI file */ + GtkWidget *app; + GtkWidget *table; + GtkWidget *map_window; + GtkWidget *timezone_combo; + GtkWidget *preview_label; +}; + +static void e_timezone_dialog_dispose (GObject *object); + +static gboolean get_widgets (ETimezoneDialog *etd); +static gboolean on_map_timeout (gpointer data); +static gboolean on_map_motion (GtkWidget *widget, + GdkEventMotion *event, + gpointer data); +static gboolean on_map_leave (GtkWidget *widget, + GdkEventCrossing *event, + gpointer data); +static gboolean on_map_visibility_changed (GtkWidget *w, + GdkEventVisibility *event, + gpointer data); +static gboolean on_map_button_pressed (GtkWidget *w, + GdkEvent *button_event, + gpointer data); + +static icaltimezone * get_zone_from_point (ETimezoneDialog *etd, + EMapPoint *point); +static void set_map_timezone (ETimezoneDialog *etd, + icaltimezone *zone); +static void on_combo_changed (GtkComboBox *combo, + ETimezoneDialog *etd); + +static void timezone_combo_get_active_text (GtkComboBox *combo, + gchar **zone_name); +static gboolean timezone_combo_set_active_text (GtkComboBox *combo, + const gchar *zone_name); + +static void map_destroy_cb (gpointer data, + GObject *where_object_was); + +G_DEFINE_TYPE (ETimezoneDialog, e_timezone_dialog, G_TYPE_OBJECT) + +/* Class initialization function for the event editor */ +static void +e_timezone_dialog_class_init (ETimezoneDialogClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ETimezoneDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = e_timezone_dialog_dispose; +} + +/* Object initialization function for the event editor */ +static void +e_timezone_dialog_init (ETimezoneDialog *etd) +{ + etd->priv = E_TIMEZONE_DIALOG_GET_PRIVATE (etd); +} + +/* Dispose handler for the event editor */ +static void +e_timezone_dialog_dispose (GObject *object) +{ + ETimezoneDialogPrivate *priv; + + priv = E_TIMEZONE_DIALOG_GET_PRIVATE (object); + + /* Destroy the actual dialog. */ + if (priv->app != NULL) { + gtk_widget_destroy (priv->app); + priv->app = NULL; + } + + if (priv->timeout_id) { + g_source_remove (priv->timeout_id); + priv->timeout_id = 0; + } + + if (priv->builder) { + g_object_unref (priv->builder); + priv->builder = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_timezone_dialog_parent_class)->dispose (object); +} + +static void +e_timezone_dialog_add_timezones (ETimezoneDialog *etd) +{ + ETimezoneDialogPrivate *priv; + icalarray *zones; + GtkComboBox *combo; + GList *l, *list_items = NULL; + GtkListStore *list_store; + GtkTreeIter iter; + GtkCellRenderer *cell; + GtkCssProvider *css_provider; + GtkStyleContext *style_context; + GHashTable *index; + const gchar *css; + gint i; + GError *error = NULL; + + priv = etd->priv; + + /* Get the array of builtin timezones. */ + zones = icaltimezone_get_builtin_timezones (); + + for (i = 0; i < zones->num_elements; i++) { + icaltimezone *zone; + gchar *location; + + zone = icalarray_element_at (zones, i); + + location = _(icaltimezone_get_location (zone)); + + e_map_add_point ( + priv->map, location, + icaltimezone_get_longitude (zone), + icaltimezone_get_latitude (zone), + E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA); + + list_items = g_list_prepend (list_items, location); + } + + list_items = g_list_sort (list_items, (GCompareFunc) g_utf8_collate); + + /* Put the "UTC" entry at the top of the combo's list. */ + list_items = g_list_prepend (list_items, _("UTC")); + + combo = GTK_COMBO_BOX (priv->timezone_combo); + + cell = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start ((GtkCellLayout *) combo, cell, TRUE); + gtk_cell_layout_set_attributes ((GtkCellLayout *) combo, cell, "text", 0, NULL); + + list_store = gtk_list_store_new (1, G_TYPE_STRING); + index = g_hash_table_new (g_str_hash, g_str_equal); + for (l = list_items, i = 0; l != NULL; l = l->next, ++i) { + gtk_list_store_append (list_store, &iter); + gtk_list_store_set (list_store, &iter, 0, (gchar *)(l->data), -1); + g_hash_table_insert (index, (gchar *)(l->data), GINT_TO_POINTER (i)); + } + + g_object_set_data_full ( + G_OBJECT (list_store), "index", index, + (GDestroyNotify) g_hash_table_destroy); + + gtk_combo_box_set_model (combo, (GtkTreeModel *) list_store); + + css_provider = gtk_css_provider_new (); + css = "GtkComboBox { -GtkComboBox-appears-as-list: 1; }"; + gtk_css_provider_load_from_data (css_provider, css, -1, &error); + style_context = gtk_widget_get_style_context (priv->timezone_combo); + if (error == NULL) { + gtk_style_context_add_provider ( + style_context, + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } else { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_clear_error (&error); + } + g_object_unref (css_provider); + + g_list_free (list_items); +} + +ETimezoneDialog * +e_timezone_dialog_construct (ETimezoneDialog *etd) +{ + ETimezoneDialogPrivate *priv; + GtkWidget *widget; + GtkWidget *map; + + g_return_val_if_fail (etd != NULL, NULL); + g_return_val_if_fail (E_IS_TIMEZONE_DIALOG (etd), NULL); + + priv = etd->priv; + + /* Load the content widgets */ + + priv->builder = gtk_builder_new (); + e_load_ui_builder_definition (priv->builder, "e-timezone-dialog.ui"); + + if (!get_widgets (etd)) { + g_message ( + "%s(): Could not find all widgets in the XML file!", + G_STRFUNC); + goto error; + } + + widget = gtk_dialog_get_content_area (GTK_DIALOG (priv->app)); + gtk_container_set_border_width (GTK_CONTAINER (widget), 0); + + widget = gtk_dialog_get_action_area (GTK_DIALOG (priv->app)); + gtk_container_set_border_width (GTK_CONTAINER (widget), 12); + + priv->map = e_map_new (); + map = GTK_WIDGET (priv->map); + + g_object_weak_ref (G_OBJECT (map), map_destroy_cb, priv); + + gtk_widget_set_events ( + map, + gtk_widget_get_events (map) | + GDK_LEAVE_NOTIFY_MASK | + GDK_VISIBILITY_NOTIFY_MASK); + + e_timezone_dialog_add_timezones (etd); + + gtk_container_add (GTK_CONTAINER (priv->map_window), map); + gtk_widget_show (map); + + /* Ensure a reasonable minimum amount of map is visible */ + gtk_widget_set_size_request (priv->map_window, 200, 200); + + g_signal_connect ( + map, "motion-notify-event", + G_CALLBACK (on_map_motion), etd); + g_signal_connect ( + map, "leave-notify-event", + G_CALLBACK (on_map_leave), etd); + g_signal_connect ( + map, "visibility-notify-event", + G_CALLBACK (on_map_visibility_changed), etd); + g_signal_connect ( + map, "button-press-event", + G_CALLBACK (on_map_button_pressed), etd); + + g_signal_connect ( + priv->timezone_combo, "changed", + G_CALLBACK (on_combo_changed), etd); + + return etd; + + error: + + g_object_unref (etd); + return NULL; +} + +#if 0 +static gint +get_local_offset (void) +{ + time_t now = time (NULL), t_gmt, t_local; + struct tm gmt, local; + gint diff; + + gmtime_r (&now, &gmt); + localtime_r (&now, &local); + t_gmt = mktime (&gmt); + t_local = mktime (&local); + diff = t_local - t_gmt; + + return diff; +} +#endif + +static icaltimezone * +get_local_timezone (void) +{ + icaltimezone *zone; + gchar *location; + + tzset (); + location = e_cal_system_timezone_get_location (); + + if (location) + zone = icaltimezone_get_builtin_timezone (location); + else + zone = icaltimezone_get_utc_timezone (); + + g_free (location); + + return zone; +} + +/* Gets the widgets from the XML file and returns if they are all available. + * For the widgets whose values can be simply set with e-dialog-utils, it does + * that as well. + */ +static gboolean +get_widgets (ETimezoneDialog *etd) +{ + ETimezoneDialogPrivate *priv; + GtkBuilder *builder; + + priv = etd->priv; + builder = etd->priv->builder; + + priv->app = e_builder_get_widget (builder, "timezone-dialog"); + priv->map_window = e_builder_get_widget (builder, "map-window"); + priv->timezone_combo = e_builder_get_widget (builder, "timezone-combo"); + priv->table = e_builder_get_widget (builder, "timezone-table"); + priv->preview_label = e_builder_get_widget (builder, "preview-label"); + + return (priv->app + && priv->map_window + && priv->timezone_combo + && priv->table + && priv->preview_label); +} + +/** + * e_timezone_dialog_new: + * + * Creates a new event editor dialog. + * + * Return value: A newly-created event editor dialog, or NULL if the event + * editor could not be created. + **/ +ETimezoneDialog * +e_timezone_dialog_new (void) +{ + ETimezoneDialog *etd; + + etd = E_TIMEZONE_DIALOG (g_object_new (E_TYPE_TIMEZONE_DIALOG, NULL)); + return e_timezone_dialog_construct (E_TIMEZONE_DIALOG (etd)); +} + +static void +format_utc_offset (gint utc_offset, + gchar *buffer) +{ + const gchar *sign = "+"; + gint hours, minutes, seconds; + + if (utc_offset < 0) { + utc_offset = -utc_offset; + sign = "-"; + } + + hours = utc_offset / 3600; + minutes = (utc_offset % 3600) / 60; + seconds = utc_offset % 60; + + /* Sanity check. Standard timezone offsets shouldn't be much more + * than 12 hours, and daylight saving shouldn't change it by more + * than a few hours. (The maximum offset is 15 hours 56 minutes + * at present.) */ + if (hours < 0 || hours >= 24 || minutes < 0 || minutes >= 60 + || seconds < 0 || seconds >= 60) { + fprintf ( + stderr, "Warning: Strange timezone offset: " + "H:%i M:%i S:%i\n", hours, minutes, seconds); + } + + if (hours == 0 && minutes == 0 && seconds == 0) + strcpy (buffer, _("UTC")); + else if (seconds == 0) + sprintf ( + buffer, "%s %s%02i:%02i", + _("UTC"), sign, hours, minutes); + else + sprintf ( + buffer, "%s %s%02i:%02i:%02i", + _("UTC"), sign, hours, minutes, seconds); +} + +static gchar * +zone_display_name_with_offset (icaltimezone *zone) +{ + const gchar *display_name; + struct tm local; + struct icaltimetype tt; + gint offset; + gchar buffer[100]; + time_t now = time (NULL); + + gmtime_r ((const time_t *) &now, &local); + tt = tm_to_icaltimetype (&local, TRUE); + offset = icaltimezone_get_utc_offset (zone, &tt, NULL); + + format_utc_offset (offset, buffer); + + display_name = icaltimezone_get_display_name (zone); + if (icaltimezone_get_builtin_timezone (display_name)) + display_name = _(display_name); + + return g_strdup_printf ("%s (%s)", display_name, buffer); +} + +static const gchar * +zone_display_name (icaltimezone *zone) +{ + const gchar *display_name; + + display_name = icaltimezone_get_display_name (zone); + if (icaltimezone_get_builtin_timezone (display_name)) + display_name = _(display_name); + + return display_name; +} + +/* This flashes the currently selected timezone in the map. */ +static gboolean +on_map_timeout (gpointer data) +{ + ETimezoneDialog *etd; + ETimezoneDialogPrivate *priv; + + etd = E_TIMEZONE_DIALOG (data); + priv = etd->priv; + + if (!priv->point_selected) + return TRUE; + + if (e_map_point_get_color_rgba (priv->point_selected) + == E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_1_RGBA) + e_map_point_set_color_rgba ( + priv->map, priv->point_selected, + E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_2_RGBA); + else + e_map_point_set_color_rgba ( + priv->map, priv->point_selected, + E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_1_RGBA); + + return TRUE; +} + +static gboolean +on_map_motion (GtkWidget *widget, + GdkEventMotion *event, + gpointer data) +{ + ETimezoneDialog *etd; + ETimezoneDialogPrivate *priv; + gdouble longitude, latitude; + icaltimezone *new_zone; + gchar *display = NULL; + + etd = E_TIMEZONE_DIALOG (data); + priv = etd->priv; + + e_map_window_to_world ( + priv->map, (gdouble) event->x, (gdouble) event->y, + &longitude, &latitude); + + if (priv->point_hover && priv->point_hover != priv->point_selected) + e_map_point_set_color_rgba ( + priv->map, priv->point_hover, + E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA); + + priv->point_hover = e_map_get_closest_point ( + priv->map, longitude, + latitude, TRUE); + + if (priv->point_hover != priv->point_selected) + e_map_point_set_color_rgba ( + priv->map, priv->point_hover, + E_TIMEZONE_DIALOG_MAP_POINT_HOVER_RGBA); + + new_zone = get_zone_from_point (etd, priv->point_hover); + + display = zone_display_name_with_offset (new_zone); + gtk_label_set_text (GTK_LABEL (priv->preview_label), display); + + g_free (display); + + return TRUE; +} + +static gboolean +on_map_leave (GtkWidget *widget, + GdkEventCrossing *event, + gpointer data) +{ + ETimezoneDialog *etd; + ETimezoneDialogPrivate *priv; + + etd = E_TIMEZONE_DIALOG (data); + priv = etd->priv; + + /* We only want to reset the hover point if this is a normal leave + * event. For some reason we are getting leave events when the + * button is pressed in the map, which causes problems. */ + if (event->mode != GDK_CROSSING_NORMAL) + return FALSE; + + if (priv->point_hover && priv->point_hover != priv->point_selected) + e_map_point_set_color_rgba ( + priv->map, priv->point_hover, + E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA); + + timezone_combo_set_active_text ( + GTK_COMBO_BOX (priv->timezone_combo), + zone_display_name (priv->zone)); + gtk_label_set_text (GTK_LABEL (priv->preview_label), ""); + + priv->point_hover = NULL; + + return FALSE; +} + +static gboolean +on_map_visibility_changed (GtkWidget *w, + GdkEventVisibility *event, + gpointer data) +{ + ETimezoneDialog *etd; + ETimezoneDialogPrivate *priv; + + etd = E_TIMEZONE_DIALOG (data); + priv = etd->priv; + + if (event->state != GDK_VISIBILITY_FULLY_OBSCURED) { + /* Map is visible, at least partly, so make sure we flash the + * selected point. */ + if (!priv->timeout_id) + priv->timeout_id = g_timeout_add (100, on_map_timeout, etd); + } else { + /* Map is invisible, so don't waste resources on the timeout.*/ + if (priv->timeout_id) { + g_source_remove (priv->timeout_id); + priv->timeout_id = 0; + } + } + + return FALSE; +} + +static gboolean +on_map_button_pressed (GtkWidget *w, + GdkEvent *button_event, + gpointer data) +{ + ETimezoneDialog *etd; + ETimezoneDialogPrivate *priv; + guint event_button = 0; + gdouble event_x_win = 0; + gdouble event_y_win = 0; + gdouble longitude, latitude; + + etd = E_TIMEZONE_DIALOG (data); + priv = etd->priv; + + gdk_event_get_button (button_event, &event_button); + gdk_event_get_coords (button_event, &event_x_win, &event_y_win); + + e_map_window_to_world ( + priv->map, event_x_win, event_y_win, &longitude, &latitude); + + if (event_button != 1) { + e_map_zoom_out (priv->map); + } else { + if (e_map_get_magnification (priv->map) <= 1.0) + e_map_zoom_to_location ( + priv->map, longitude, latitude); + + if (priv->point_selected) + e_map_point_set_color_rgba ( + priv->map, + priv->point_selected, + E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA); + priv->point_selected = priv->point_hover; + + priv->zone = get_zone_from_point (etd, priv->point_selected); + timezone_combo_set_active_text ( + GTK_COMBO_BOX (priv->timezone_combo), + zone_display_name (priv->zone)); + } + + return TRUE; +} + +/* Returns the translated timezone location of the given EMapPoint, + * e.g. "Europe/London". */ +static icaltimezone * +get_zone_from_point (ETimezoneDialog *etd, + EMapPoint *point) +{ + icalarray *zones; + gdouble longitude, latitude; + gint i; + + if (point == NULL) + return NULL; + + e_map_point_get_location (point, &longitude, &latitude); + + /* Get the array of builtin timezones. */ + zones = icaltimezone_get_builtin_timezones (); + + for (i = 0; i < zones->num_elements; i++) { + icaltimezone *zone; + gdouble zone_longitude, zone_latitude; + + zone = icalarray_element_at (zones, i); + zone_longitude = icaltimezone_get_longitude (zone); + zone_latitude = icaltimezone_get_latitude (zone); + + if (zone_longitude - 0.005 <= longitude && + zone_longitude + 0.005 >= longitude && + zone_latitude - 0.005 <= latitude && + zone_latitude + 0.005 >= latitude) + { + return zone; + } + } + + g_return_val_if_reached (NULL); +} + +/** + * e_timezone_dialog_get_timezone: + * @etd: the timezone dialog + * + * Return value: the currently-selected timezone, or %NULL if no timezone + * is selected. + **/ +icaltimezone * +e_timezone_dialog_get_timezone (ETimezoneDialog *etd) +{ + ETimezoneDialogPrivate *priv; + + g_return_val_if_fail (E_IS_TIMEZONE_DIALOG (etd), NULL); + + priv = etd->priv; + + return priv->zone; +} + +/** + * e_timezone_dialog_set_timezone: + * @etd: the timezone dialog + * @zone: the timezone + * + * Sets the timezone of @etd to @zone. Updates the display name and + * selected location. The caller must ensure that @zone is not freed + * before @etd is destroyed. + **/ + +void +e_timezone_dialog_set_timezone (ETimezoneDialog *etd, + icaltimezone *zone) +{ + ETimezoneDialogPrivate *priv; + gchar *display = NULL; + + g_return_if_fail (E_IS_TIMEZONE_DIALOG (etd)); + + if (!zone) + zone = get_local_timezone (); + + if (zone) + display = zone_display_name_with_offset (zone); + + priv = etd->priv; + + priv->zone = zone; + + gtk_label_set_text ( + GTK_LABEL (priv->preview_label), + zone ? display : ""); + timezone_combo_set_active_text ( + GTK_COMBO_BOX (priv->timezone_combo), + zone ? zone_display_name (zone) : ""); + + set_map_timezone (etd, zone); + g_free (display); +} + +GtkWidget * +e_timezone_dialog_get_toplevel (ETimezoneDialog *etd) +{ + ETimezoneDialogPrivate *priv; + + g_return_val_if_fail (etd != NULL, NULL); + g_return_val_if_fail (E_IS_TIMEZONE_DIALOG (etd), NULL); + + priv = etd->priv; + + return priv->app; +} + +static void +set_map_timezone (ETimezoneDialog *etd, + icaltimezone *zone) +{ + ETimezoneDialogPrivate *priv; + EMapPoint *point; + gdouble zone_longitude, zone_latitude; + + priv = etd->priv; + + if (zone) { + zone_longitude = icaltimezone_get_longitude (zone); + zone_latitude = icaltimezone_get_latitude (zone); + point = e_map_get_closest_point ( + priv->map, + zone_longitude, + zone_latitude, + FALSE); + } else + point = NULL; + + if (priv->point_selected) + e_map_point_set_color_rgba ( + priv->map, priv->point_selected, + E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA); + + priv->point_selected = point; +} + +static void +on_combo_changed (GtkComboBox *combo_box, + ETimezoneDialog *etd) +{ + ETimezoneDialogPrivate *priv; + gchar *new_zone_name; + icalarray *zones; + icaltimezone *map_zone = NULL; + gchar *location; + gint i; + + priv = etd->priv; + + timezone_combo_get_active_text ( + GTK_COMBO_BOX (priv->timezone_combo), &new_zone_name); + + if (!new_zone_name || !*new_zone_name) + priv->zone = NULL; + else if (!g_utf8_collate (new_zone_name, _("UTC"))) + priv->zone = icaltimezone_get_utc_timezone (); + else { + priv->zone = NULL; + + zones = icaltimezone_get_builtin_timezones (); + for (i = 0; i < zones->num_elements; i++) { + map_zone = icalarray_element_at (zones, i); + location = _(icaltimezone_get_location (map_zone)); + if (!g_utf8_collate (new_zone_name, location)) { + priv->zone = map_zone; + break; + } + } + } + + set_map_timezone (etd, map_zone); + + g_free (new_zone_name); +} + +static void +timezone_combo_get_active_text (GtkComboBox *combo, + gchar **zone_name) +{ + GtkTreeModel *list_store; + GtkTreeIter iter; + + list_store = gtk_combo_box_get_model (combo); + + /* Get the active iter in the list */ + if (gtk_combo_box_get_active_iter (combo, &iter)) + gtk_tree_model_get (list_store, &iter, 0, zone_name, -1); + else + *zone_name = NULL; +} + +static gboolean +timezone_combo_set_active_text (GtkComboBox *combo, + const gchar *zone_name) +{ + GtkTreeModel *list_store; + GHashTable *index; + gpointer id = NULL; + + list_store = gtk_combo_box_get_model (combo); + index = (GHashTable *) g_object_get_data (G_OBJECT (list_store), "index"); + + if (zone_name && *zone_name) + id = g_hash_table_lookup (index, zone_name); + + gtk_combo_box_set_active (combo, GPOINTER_TO_INT (id)); + + return (id != NULL); +} + +static void +map_destroy_cb (gpointer data, + GObject *where_object_was) +{ + + ETimezoneDialogPrivate *priv = data; + if (priv->timeout_id) { + g_source_remove (priv->timeout_id); + priv->timeout_id = 0; + } + return; +} diff --git a/e-util/e-timezone-dialog.h b/e-util/e-timezone-dialog.h new file mode 100644 index 0000000000..df87e80941 --- /dev/null +++ b/e-util/e-timezone-dialog.h @@ -0,0 +1,77 @@ +/* + * Evolution calendar - Timezone selector dialog + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_TIMEZONE_DIALOG_H +#define E_TIMEZONE_DIALOG_H + +#include <gtk/gtk.h> +#include <libical/ical.h> + +/* Standard GObject macros */ +#define E_TYPE_TIMEZONE_DIALOG \ + (e_timezone_dialog_get_type ()) +#define E_TIMEZONE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TIMEZONE_DIALOG, ETimezoneDialog)) +#define E_TIMEZONE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TIMEZONE_DIALOG, ETimezoneDialogClass)) +#define E_IS_TIMEZONE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TIMEZONE_DIALOG)) +#define E_IS_TIMEZONE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TIMEZONE_DIALOG)) +#define E_TIMEZONE_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TIMEZONE_DIALOG, ETimezoneDialogClass)) + +typedef struct _ETimezoneDialog ETimezoneDialog; +typedef struct _ETimezoneDialogClass ETimezoneDialogClass; +typedef struct _ETimezoneDialogPrivate ETimezoneDialogPrivate; + +struct _ETimezoneDialog { + GObject object; + ETimezoneDialogPrivate *priv; +}; + +struct _ETimezoneDialogClass { + GObjectClass parent_class; +}; + +GType e_timezone_dialog_get_type (void); +ETimezoneDialog * + e_timezone_dialog_construct (ETimezoneDialog *etd); +ETimezoneDialog * + e_timezone_dialog_new (void); +icaltimezone * e_timezone_dialog_get_timezone (ETimezoneDialog *etd); +void e_timezone_dialog_set_timezone (ETimezoneDialog *etd, + icaltimezone *zone); +GtkWidget * e_timezone_dialog_get_toplevel (ETimezoneDialog *etd); + +#endif /* E_TIMEZONE_DIALOG_H */ diff --git a/e-util/e-timezone-dialog.ui b/e-util/e-timezone-dialog.ui new file mode 100644 index 0000000000..5a23ec180e --- /dev/null +++ b/e-util/e-timezone-dialog.ui @@ -0,0 +1,312 @@ +<?xml version="1.0"?> +<!--*- mode: xml -*--> +<interface> + <object class="GtkDialog" id="timezone-dialog"> + <property name="title" translatable="yes">Select a Time Zone</property> + <property name="type">GTK_WINDOW_TOPLEVEL</property> + <property name="window_position">GTK_WIN_POS_NONE</property> + <property name="modal">False</property> + <property name="default_width">500</property> + <property name="default_height">400</property> + <property name="resizable">True</property> + <property name="destroy_with_parent">False</property> + <property name="decorated">True</property> + <property name="skip_taskbar_hint">False</property> + <property name="skip_pager_hint">False</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property> + <property name="gravity">GDK_GRAVITY_NORTH_WEST</property> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">6</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">GTK_BUTTONBOX_END</property> + <child> + <object class="GtkButton" id="cancel-button"> + <property name="visible">True</property> + <property name="can_default">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-cancel</property> + <property name="use_stock">True</property> + <property name="relief">GTK_RELIEF_NORMAL</property> + <property name="focus_on_click">True</property> + </object> + </child> + <child> + <object class="GtkButton" id="ok-button"> + <property name="visible">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-ok</property> + <property name="use_stock">True</property> + <property name="relief">GTK_RELIEF_NORMAL</property> + <property name="focus_on_click">True</property> + </object> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + <child> + <object class="GtkBox" id="timezone-table"> + <property name="orientation">vertical</property> + <property name="border_width">12</property> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkBox" id="hbox3"> + <property name="orientation">horizontal</property> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-dialog-info</property> + <property name="icon_size">6</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Use the left mouse button to zoom in on an area of the map and select a time zone. +Use the right mouse button to zoom out.</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="label" translatable="yes">Time Zones</property> + <property name="use_underline">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox2"> + <property name="orientation">horizontal</property> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="label" translatable="no"> </property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox1"> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkScrolledWindow" id="map-window"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_ALWAYS</property> + <property name="vscrollbar_policy">GTK_POLICY_ALWAYS</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="window_placement">GTK_CORNER_TOP_LEFT</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="preview-label"> + <property name="visible">True</property> + <property name="label" translatable="no">America/New_York</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Selection</property> + <property name="use_underline">True</property> + <property name="justify">GTK_JUSTIFY_CENTER</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="mnemonic_widget">timezone-combo</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox1"> + <property name="orientation">horizontal</property> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="label" translatable="no"> </property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="timezone-combo"> + <property name="visible">True</property> + <property name="add_tearoffs">False</property> + <property name="focus_on_click">True</property> + <accessibility> + + </accessibility> + <child internal-child="accessible"> + <object class="AtkObject" id="a11y-timezone-combo1"> + <property name="AtkObject::accessible_name" translatable="yes">Timezone drop-down combination box</property> + </object> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-2">cancel-button</action-widget> + <action-widget response="-3">ok-button</action-widget> + </action-widgets> + </object> +</interface> diff --git a/e-util/e-tree-memory-callbacks.c b/e-util/e-tree-memory-callbacks.c new file mode 100644 index 0000000000..9d2fda6e3e --- /dev/null +++ b/e-util/e-tree-memory-callbacks.c @@ -0,0 +1,314 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> + +#include "e-tree-memory-callbacks.h" + +G_DEFINE_TYPE (ETreeMemoryCallbacks, e_tree_memory_callbacks, E_TYPE_TREE_MEMORY) + +static GdkPixbuf * +etmc_icon_at (ETreeModel *etm, + ETreePath node) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + return etmc->icon_at (etm, node, etmc->model_data); +} + +static gint +etmc_column_count (ETreeModel *etm) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->column_count) + return etmc->column_count (etm, etmc->model_data); + else + return 0; +} + +static gboolean +etmc_has_save_id (ETreeModel *etm) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->has_save_id) + return etmc->has_save_id (etm, etmc->model_data); + else + return FALSE; +} + +static gchar * +etmc_get_save_id (ETreeModel *etm, + ETreePath node) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->get_save_id) + return etmc->get_save_id (etm, node, etmc->model_data); + else + return NULL; +} + +static gboolean +etmc_has_get_node_by_id (ETreeModel *etm) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->has_get_node_by_id) + return etmc->has_get_node_by_id (etm, etmc->model_data); + else + return FALSE; +} + +static ETreePath +etmc_get_node_by_id (ETreeModel *etm, + const gchar *save_id) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->get_node_by_id) + return etmc->get_node_by_id (etm, save_id, etmc->model_data); + else + return NULL; +} + +static gpointer +etmc_sort_value_at (ETreeModel *etm, + ETreePath node, + gint col) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->sort_value_at) + return etmc->sort_value_at (etm, node, col, etmc->model_data); + else + return etmc->value_at (etm, node, col, etmc->model_data); +} + +static gpointer +etmc_value_at (ETreeModel *etm, + ETreePath node, + gint col) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + return etmc->value_at (etm, node, col, etmc->model_data); +} + +static void +etmc_set_value_at (ETreeModel *etm, + ETreePath node, + gint col, + gconstpointer val) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + etmc->set_value_at (etm, node, col, val, etmc->model_data); +} + +static gboolean +etmc_is_editable (ETreeModel *etm, + ETreePath node, + gint col) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + return etmc->is_editable (etm, node, col, etmc->model_data); +} + +/* The default for etmc_duplicate_value is to return the raw value. */ +static gpointer +etmc_duplicate_value (ETreeModel *etm, + gint col, + gconstpointer value) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->duplicate_value) + return etmc->duplicate_value (etm, col, value, etmc->model_data); + else + return (gpointer) value; +} + +static void +etmc_free_value (ETreeModel *etm, + gint col, + gpointer value) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->free_value) + etmc->free_value (etm, col, value, etmc->model_data); +} + +static gpointer +etmc_initialize_value (ETreeModel *etm, + gint col) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->initialize_value) + return etmc->initialize_value (etm, col, etmc->model_data); + else + return NULL; +} + +static gboolean +etmc_value_is_empty (ETreeModel *etm, + gint col, + gconstpointer value) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->value_is_empty) + return etmc->value_is_empty (etm, col, value, etmc->model_data); + else + return FALSE; +} + +static gchar * +etmc_value_to_string (ETreeModel *etm, + gint col, + gconstpointer value) +{ + ETreeMemoryCallbacks *etmc = E_TREE_MEMORY_CALLBACKS (etm); + + if (etmc->value_to_string) + return etmc->value_to_string (etm, col, value, etmc->model_data); + else + return g_strdup (""); +} + +static void +e_tree_memory_callbacks_class_init (ETreeMemoryCallbacksClass *class) +{ + ETreeModelClass *model_class = E_TREE_MODEL_CLASS (class); + + model_class->icon_at = etmc_icon_at; + + model_class->column_count = etmc_column_count; + + model_class->has_save_id = etmc_has_save_id; + model_class->get_save_id = etmc_get_save_id; + + model_class->has_get_node_by_id = etmc_has_get_node_by_id; + model_class->get_node_by_id = etmc_get_node_by_id; + + model_class->sort_value_at = etmc_sort_value_at; + model_class->value_at = etmc_value_at; + model_class->set_value_at = etmc_set_value_at; + model_class->is_editable = etmc_is_editable; + + model_class->duplicate_value = etmc_duplicate_value; + model_class->free_value = etmc_free_value; + model_class->initialize_value = etmc_initialize_value; + model_class->value_is_empty = etmc_value_is_empty; + model_class->value_to_string = etmc_value_to_string; +} + +static void +e_tree_memory_callbacks_init (ETreeMemoryCallbacks *etmc) +{ + /* nothing to do */ +} + +/** + * e_tree_memory_callbacks_new: + * + * This initializes a new ETreeMemoryCallbacksModel object. + * ETreeMemoryCallbacksModel is an implementaiton of the somewhat + * abstract class ETreeMemory. The ETreeMemoryCallbacksModel is + * designed to allow people to easily create ETreeMemorys without + * having to create a new GType derived from ETreeMemory every time + * they need one. + * + * Instead, ETreeMemoryCallbacksModel uses a setup based in callback functions, every + * callback function signature mimics the signature of each ETreeModel method + * and passes the extra @data pointer to each one of the method to provide them + * with any context they might want to use. + * + * ETreeMemoryCallbacks is to ETreeMemory as ETableSimple is to ETableModel. + * + * Return value: An ETreeMemoryCallbacks object (which is also an + * ETreeMemory and thus an ETreeModel object). + * + */ +ETreeModel * +e_tree_memory_callbacks_new (ETreeMemoryCallbacksIconAtFn icon_at, + + ETreeMemoryCallbacksColumnCountFn column_count, + + ETreeMemoryCallbacksHasSaveIdFn has_save_id, + ETreeMemoryCallbacksGetSaveIdFn get_save_id, + + ETreeMemoryCallbacksHasGetNodeByIdFn has_get_node_by_id, + ETreeMemoryCallbacksGetNodeByIdFn get_node_by_id, + + ETreeMemoryCallbacksValueAtFn sort_value_at, + ETreeMemoryCallbacksValueAtFn value_at, + ETreeMemoryCallbacksSetValueAtFn set_value_at, + ETreeMemoryCallbacksIsEditableFn is_editable, + + ETreeMemoryCallbacksDuplicateValueFn duplicate_value, + ETreeMemoryCallbacksFreeValueFn free_value, + ETreeMemoryCallbacksInitializeValueFn initialize_value, + ETreeMemoryCallbacksValueIsEmptyFn value_is_empty, + ETreeMemoryCallbacksValueToStringFn value_to_string, + + gpointer model_data) +{ + ETreeMemoryCallbacks *etmc; + + etmc = g_object_new (E_TYPE_TREE_MEMORY_CALLBACKS, NULL); + + etmc->icon_at = icon_at; + + etmc->column_count = column_count; + + etmc->has_save_id = has_save_id; + etmc->get_save_id = get_save_id; + + etmc->has_get_node_by_id = has_get_node_by_id; + etmc->get_node_by_id = get_node_by_id; + + etmc->sort_value_at = sort_value_at; + etmc->value_at = value_at; + etmc->set_value_at = set_value_at; + etmc->is_editable = is_editable; + + etmc->duplicate_value = duplicate_value; + etmc->free_value = free_value; + etmc->initialize_value = initialize_value; + etmc->value_is_empty = value_is_empty; + etmc->value_to_string = value_to_string; + + etmc->model_data = model_data; + + return (ETreeModel *) etmc; +} + diff --git a/e-util/e-tree-memory-callbacks.h b/e-util/e-tree-memory-callbacks.h new file mode 100644 index 0000000000..df47e9a491 --- /dev/null +++ b/e-util/e-tree-memory-callbacks.h @@ -0,0 +1,182 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TREE_MEMORY_CALLBACKS_H_ +#define _E_TREE_MEMORY_CALLBACKS_H_ + +#include <e-util/e-tree-memory.h> + +/* Standard GObject macros */ +#define E_TYPE_TREE_MEMORY_CALLBACKS \ + (e_tree_memory_callbacks_get_type ()) +#define E_TREE_MEMORY_CALLBACKS(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TREE_MEMORY_CALLBACKS, ETreeMemoryCallbacks)) +#define E_TREE_MEMORY_CALLBACKS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TREE_MEMORY_CALLBACKS, ETreeMemoryCallbacksClass)) +#define E_IS_TREE_MEMORY_CALLBACKS(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TREE_MEMORY_CALLBACKS)) +#define E_IS_TREE_MEMORY_CALLBACKS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TREE_MEMORY_CALLBACKS)) +#define E_TREE_MEMORY_CALLBACKS_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TREE_MEMORY_CALLBACKS, ETreeMemoryCallbacksClass)) + +G_BEGIN_DECLS + +typedef struct _ETreeMemoryCallbacks ETreeMemoryCallbacks; +typedef struct _ETreeMemoryCallbacksClass ETreeMemoryCallbacksClass; + +typedef GdkPixbuf * (*ETreeMemoryCallbacksIconAtFn) + (ETreeModel *etree, + ETreePath path, + gpointer model_data); + +typedef gint (*ETreeMemoryCallbacksColumnCountFn) + (ETreeModel *etree, + gpointer model_data); + +typedef gboolean (*ETreeMemoryCallbacksHasSaveIdFn) + (ETreeModel *etree, + gpointer model_data); +typedef gchar * (*ETreeMemoryCallbacksGetSaveIdFn) + (ETreeModel *etree, + ETreePath path, + gpointer model_data); + +typedef gboolean (*ETreeMemoryCallbacksHasGetNodeByIdFn) + (ETreeModel *etree, + gpointer model_data); +typedef ETreePath (*ETreeMemoryCallbacksGetNodeByIdFn) + (ETreeModel *etree, + const gchar *save_id, + gpointer model_data); + +typedef gpointer (*ETreeMemoryCallbacksValueAtFn) + (ETreeModel *etree, + ETreePath path, + gint col, + gpointer model_data); +typedef void (*ETreeMemoryCallbacksSetValueAtFn) + (ETreeModel *etree, + ETreePath path, + gint col, + gconstpointer val, + gpointer model_data); +typedef gboolean (*ETreeMemoryCallbacksIsEditableFn) + (ETreeModel *etree, + ETreePath path, + gint col, + gpointer model_data); + +typedef gpointer (*ETreeMemoryCallbacksDuplicateValueFn) + (ETreeModel *etm, + gint col, + gconstpointer val, + gpointer data); +typedef void (*ETreeMemoryCallbacksFreeValueFn) + (ETreeModel *etm, + gint col, + gpointer val, + gpointer data); +typedef gpointer (*ETreeMemoryCallbacksInitializeValueFn) + (ETreeModel *etm, + gint col, + gpointer data); +typedef gboolean (*ETreeMemoryCallbacksValueIsEmptyFn) + (ETreeModel *etm, + gint col, + gconstpointer val, + gpointer data); +typedef gchar * (*ETreeMemoryCallbacksValueToStringFn) + (ETreeModel *etm, + gint col, + gconstpointer val, + gpointer data); + +struct _ETreeMemoryCallbacks { + ETreeMemory parent; + + ETreeMemoryCallbacksIconAtFn icon_at; + + ETreeMemoryCallbacksColumnCountFn column_count; + + ETreeMemoryCallbacksHasSaveIdFn has_save_id; + ETreeMemoryCallbacksGetSaveIdFn get_save_id; + + ETreeMemoryCallbacksHasGetNodeByIdFn has_get_node_by_id; + ETreeMemoryCallbacksGetNodeByIdFn get_node_by_id; + + ETreeMemoryCallbacksValueAtFn sort_value_at; + ETreeMemoryCallbacksValueAtFn value_at; + ETreeMemoryCallbacksSetValueAtFn set_value_at; + ETreeMemoryCallbacksIsEditableFn is_editable; + + ETreeMemoryCallbacksDuplicateValueFn duplicate_value; + ETreeMemoryCallbacksFreeValueFn free_value; + ETreeMemoryCallbacksInitializeValueFn initialize_value; + ETreeMemoryCallbacksValueIsEmptyFn value_is_empty; + ETreeMemoryCallbacksValueToStringFn value_to_string; + + gpointer model_data; +}; + +struct _ETreeMemoryCallbacksClass { + ETreeMemoryClass parent_class; +}; + +GType e_tree_memory_callbacks_get_type + (void) G_GNUC_CONST; +ETreeModel * e_tree_memory_callbacks_new + (ETreeMemoryCallbacksIconAtFn icon_at, + + ETreeMemoryCallbacksColumnCountFn column_count, + + ETreeMemoryCallbacksHasSaveIdFn has_save_id, + ETreeMemoryCallbacksGetSaveIdFn get_save_id, + + ETreeMemoryCallbacksHasGetNodeByIdFn has_get_node_by_id, + ETreeMemoryCallbacksGetNodeByIdFn get_node_by_id, + + ETreeMemoryCallbacksValueAtFn sort_value_at, + ETreeMemoryCallbacksValueAtFn value_at, + ETreeMemoryCallbacksSetValueAtFn set_value_at, + ETreeMemoryCallbacksIsEditableFn is_editable, + + ETreeMemoryCallbacksDuplicateValueFn duplicate_value, + ETreeMemoryCallbacksFreeValueFn free_value, + ETreeMemoryCallbacksInitializeValueFn initialize_value, + ETreeMemoryCallbacksValueIsEmptyFn value_is_empty, + ETreeMemoryCallbacksValueToStringFn value_to_string, + + gpointer model_data); + +G_END_DECLS + +#endif /* _E_TREE_MEMORY_CALLBACKS_H_ */ diff --git a/e-util/e-tree-memory.c b/e-util/e-tree-memory.c new file mode 100644 index 0000000000..0af5d27b31 --- /dev/null +++ b/e-util/e-tree-memory.c @@ -0,0 +1,743 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Chris Toshok <toshok@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-tree-memory.h" + +#include <stdio.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdlib.h> + +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include "e-xml-utils.h" + +#define E_TREE_MEMORY_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TREE_MEMORY, ETreeMemoryPrivate)) + +G_DEFINE_TYPE (ETreeMemory, e_tree_memory, E_TYPE_TREE_MODEL) + +enum { + FILL_IN_CHILDREN, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +typedef struct ETreeMemoryPath ETreeMemoryPath; + +struct ETreeMemoryPath { + gpointer node_data; + + guint children_computed : 1; + + /* parent/child/sibling pointers */ + ETreeMemoryPath *parent; + ETreeMemoryPath *next_sibling; + ETreeMemoryPath *prev_sibling; + ETreeMemoryPath *first_child; + ETreeMemoryPath *last_child; + + gint num_children; +}; + +struct _ETreeMemoryPrivate { + ETreeMemoryPath *root; + + /* whether nodes are created expanded + * or collapsed by default */ + gboolean expanded_default; + + gint frozen; + GFunc destroy_func; + gpointer destroy_user_data; +}; + +/* ETreeMemoryPath functions */ + +static inline void +check_children (ETreeMemory *memory, + ETreePath node) +{ + ETreeMemoryPath *path = node; + if (!path->children_computed) { + g_signal_emit (memory, signals[FILL_IN_CHILDREN], 0, node); + path->children_computed = TRUE; + } +} + +static gint +e_tree_memory_path_depth (ETreeMemoryPath *path) +{ + gint depth = 0; + + g_return_val_if_fail (path != NULL, -1); + + for (path = path->parent; path; path = path->parent) + depth++; + return depth; +} + +static void +e_tree_memory_path_insert (ETreeMemoryPath *parent, + gint position, + ETreeMemoryPath *child) +{ + g_return_if_fail (position <= parent->num_children && position >= -1); + + child->parent = parent; + + if (parent->first_child == NULL) + parent->first_child = child; + + if (position == -1 || position == parent->num_children) { + child->prev_sibling = parent->last_child; + if (parent->last_child) + parent->last_child->next_sibling = child; + parent->last_child = child; + } else { + ETreeMemoryPath *c; + for (c = parent->first_child; c; c = c->next_sibling) { + if (position == 0) { + child->next_sibling = c; + child->prev_sibling = c->prev_sibling; + + if (child->next_sibling) + child->next_sibling->prev_sibling = child; + if (child->prev_sibling) + child->prev_sibling->next_sibling = child; + + if (parent->first_child == c) + parent->first_child = child; + break; + } + position--; + } + } + + parent->num_children++; +} + +static void +e_tree_path_unlink (ETreeMemoryPath *path) +{ + ETreeMemoryPath *parent = path->parent; + + /* unlink first/last child if applicable */ + if (parent) { + if (path == parent->first_child) + parent->first_child = path->next_sibling; + if (path == parent->last_child) + parent->last_child = path->prev_sibling; + + parent->num_children--; + } + + /* unlink prev/next sibling links */ + if (path->next_sibling) + path->next_sibling->prev_sibling = path->prev_sibling; + if (path->prev_sibling) + path->prev_sibling->next_sibling = path->next_sibling; + + path->parent = NULL; + path->next_sibling = NULL; + path->prev_sibling = NULL; +} + +/** + * e_tree_memory_freeze: + * @etmm: the ETreeModel to freeze. + * + * This function prepares an ETreeModel for a period of much change. + * All signals regarding changes to the tree are deferred until we + * thaw the tree. + * + **/ +void +e_tree_memory_freeze (ETreeMemory *etmm) +{ + ETreeMemoryPrivate *priv = etmm->priv; + + if (priv->frozen == 0) + e_tree_model_pre_change (E_TREE_MODEL (etmm)); + + priv->frozen++; +} + +/** + * e_tree_memory_thaw: + * @etmm: the ETreeMemory to thaw. + * + * This function thaws an ETreeMemory. All the defered signals can add + * up to a lot, we don't know - so we just emit a model_changed + * signal. + * + **/ +void +e_tree_memory_thaw (ETreeMemory *etmm) +{ + ETreeMemoryPrivate *priv = etmm->priv; + + if (priv->frozen > 0) + priv->frozen--; + if (priv->frozen == 0) { + e_tree_model_node_changed (E_TREE_MODEL (etmm), priv->root); + } +} + +/* virtual methods */ + +static void +etmm_dispose (GObject *object) +{ + ETreeMemoryPrivate *priv; + + priv = E_TREE_MEMORY_GET_PRIVATE (object); + + if (priv->root) + e_tree_memory_node_remove ( + E_TREE_MEMORY (object), priv->root); + + G_OBJECT_CLASS (e_tree_memory_parent_class)->dispose (object); +} + +static ETreePath +etmm_get_root (ETreeModel *etm) +{ + ETreeMemoryPrivate *priv = E_TREE_MEMORY (etm)->priv; + return priv->root; +} + +static ETreePath +etmm_get_parent (ETreeModel *etm, + ETreePath node) +{ + ETreeMemoryPath *path = node; + return path->parent; +} + +static ETreePath +etmm_get_first_child (ETreeModel *etm, + ETreePath node) +{ + ETreeMemoryPath *path = node; + + check_children (E_TREE_MEMORY (etm), node); + return path->first_child; +} + +static ETreePath +etmm_get_last_child (ETreeModel *etm, + ETreePath node) +{ + ETreeMemoryPath *path = node; + + check_children (E_TREE_MEMORY (etm), node); + return path->last_child; +} + +static ETreePath +etmm_get_next (ETreeModel *etm, + ETreePath node) +{ + ETreeMemoryPath *path = node; + return path->next_sibling; +} + +static ETreePath +etmm_get_prev (ETreeModel *etm, + ETreePath node) +{ + ETreeMemoryPath *path = node; + return path->prev_sibling; +} + +static gboolean +etmm_is_root (ETreeModel *etm, + ETreePath node) +{ + ETreeMemoryPath *path = node; + return e_tree_memory_path_depth (path) == 0; +} + +static gboolean +etmm_is_expandable (ETreeModel *etm, + ETreePath node) +{ + ETreeMemoryPath *path = node; + + check_children (E_TREE_MEMORY (etm), node); + return path->first_child != NULL; +} + +static guint +etmm_get_children (ETreeModel *etm, + ETreePath node, + ETreePath **nodes) +{ + ETreeMemoryPath *path = node; + guint n_children; + + check_children (E_TREE_MEMORY (etm), node); + + n_children = path->num_children; + + if (nodes) { + ETreeMemoryPath *p; + gint i = 0; + + (*nodes) = g_new (ETreePath, n_children); + for (p = path->first_child; p; p = p->next_sibling) { + (*nodes)[i++] = p; + } + } + + return n_children; +} + +static guint +etmm_depth (ETreeModel *etm, + ETreePath path) +{ + return e_tree_memory_path_depth (path); +} + +static gboolean +etmm_get_expanded_default (ETreeModel *etm) +{ + ETreeMemory *etmm = E_TREE_MEMORY (etm); + ETreeMemoryPrivate *priv = etmm->priv; + + return priv->expanded_default; +} + +static void +etmm_clear_children_computed (ETreeMemoryPath *path) +{ + for (path = path->first_child; path; path = path->next_sibling) { + path->children_computed = FALSE; + etmm_clear_children_computed (path); + } +} + +static void +etmm_node_request_collapse (ETreeModel *etm, + ETreePath node) +{ + ETreeModelClass *parent_class; + + if (node) + etmm_clear_children_computed (node); + + parent_class = E_TREE_MODEL_CLASS (e_tree_memory_parent_class); + + if (parent_class->node_request_collapse != NULL) + parent_class->node_request_collapse (etm, node); +} + +static void +e_tree_memory_class_init (ETreeMemoryClass *class) +{ + GObjectClass *object_class; + ETreeModelClass *tree_model_class; + + g_type_class_add_private (class, sizeof (ETreeMemoryPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = etmm_dispose; + + tree_model_class = E_TREE_MODEL_CLASS (class); + tree_model_class->get_root = etmm_get_root; + tree_model_class->get_prev = etmm_get_prev; + tree_model_class->get_next = etmm_get_next; + tree_model_class->get_first_child = etmm_get_first_child; + tree_model_class->get_last_child = etmm_get_last_child; + tree_model_class->get_parent = etmm_get_parent; + + tree_model_class->is_root = etmm_is_root; + tree_model_class->is_expandable = etmm_is_expandable; + tree_model_class->get_children = etmm_get_children; + tree_model_class->depth = etmm_depth; + tree_model_class->get_expanded_default = etmm_get_expanded_default; + + tree_model_class->node_request_collapse = etmm_node_request_collapse; + + class->fill_in_children = NULL; + + signals[FILL_IN_CHILDREN] = g_signal_new ( + "fill_in_children", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeMemoryClass, fill_in_children), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); +} + +static void +e_tree_memory_init (ETreeMemory *etmm) +{ + etmm->priv = E_TREE_MEMORY_GET_PRIVATE (etmm); +} + +/** + * e_tree_memory_construct: + * @etree: + * + * + **/ +void +e_tree_memory_construct (ETreeMemory *etmm) +{ +} + +/** + * e_tree_memory_new + * + * XXX docs here. + * + * return values: a newly constructed ETreeMemory. + */ +ETreeMemory * +e_tree_memory_new (void) +{ + return g_object_new (E_TYPE_TREE_MEMORY, NULL); +} + +/** + * e_tree_memory_set_expanded_default + * + * Sets the state of nodes to be append to a thread. + * They will either be expanded or collapsed, according to + * the value of @expanded. + */ +void +e_tree_memory_set_expanded_default (ETreeMemory *etree, + gboolean expanded) +{ + g_return_if_fail (etree != NULL); + + etree->priv->expanded_default = expanded; +} + +/** + * e_tree_memory_node_get_data: + * @etmm: + * @node: + * + * + * + * Return value: + **/ +gpointer +e_tree_memory_node_get_data (ETreeMemory *etmm, + ETreePath node) +{ + ETreeMemoryPath *path = node; + + g_return_val_if_fail (path, NULL); + + return path->node_data; +} + +/** + * e_tree_memory_node_set_data: + * @etmm: + * @node: + * @node_data: + * + * + **/ +void +e_tree_memory_node_set_data (ETreeMemory *etmm, + ETreePath node, + gpointer node_data) +{ + ETreeMemoryPath *path = node; + + g_return_if_fail (path); + + path->node_data = node_data; +} + +/** + * e_tree_memory_node_insert: + * @tree_model: + * @parent_path: + * @position: + * @node_data: + * + * + * + * Return value: + **/ +ETreePath +e_tree_memory_node_insert (ETreeMemory *tree_model, + ETreePath parent_node, + gint position, + gpointer node_data) +{ + ETreeMemoryPrivate *priv; + ETreeMemoryPath *new_path; + ETreeMemoryPath *parent_path = parent_node; + + g_return_val_if_fail (tree_model != NULL, NULL); + + priv = tree_model->priv; + + g_return_val_if_fail (parent_path != NULL || priv->root == NULL, NULL); + + priv = tree_model->priv; + + if (!tree_model->priv->frozen) + e_tree_model_pre_change (E_TREE_MODEL (tree_model)); + + new_path = g_slice_new0 (ETreeMemoryPath); + + new_path->node_data = node_data; + new_path->children_computed = FALSE; + + if (parent_path != NULL) { + e_tree_memory_path_insert (parent_path, position, new_path); + if (!tree_model->priv->frozen) + e_tree_model_node_inserted ( + E_TREE_MODEL (tree_model), + parent_path, new_path); + } else { + priv->root = new_path; + if (!tree_model->priv->frozen) + e_tree_model_node_changed ( + E_TREE_MODEL (tree_model), new_path); + } + + return new_path; +} + +ETreePath +e_tree_memory_node_insert_id (ETreeMemory *etree, + ETreePath parent, + gint position, + gpointer node_data, + gchar *id) +{ + return e_tree_memory_node_insert (etree, parent, position, node_data); +} + +/** + * e_tree_memory_node_insert_before: + * @etree: + * @parent: + * @sibling: + * @node_data: + * + * + * + * Return value: + **/ +ETreePath +e_tree_memory_node_insert_before (ETreeMemory *etree, + ETreePath parent, + ETreePath sibling, + gpointer node_data) +{ + ETreeMemoryPath *child; + ETreeMemoryPath *parent_path = parent; + ETreeMemoryPath *sibling_path = sibling; + gint position = 0; + + g_return_val_if_fail (etree != NULL, NULL); + + if (sibling != NULL) { + for (child = parent_path->first_child; child; child = child->next_sibling) { + if (child == sibling_path) + break; + position++; + } + } else + position = parent_path->num_children; + return e_tree_memory_node_insert (etree, parent, position, node_data); +} + +/* just blows away child data, doesn't take into account unlinking/etc */ +static void +child_free (ETreeMemory *etree, + ETreeMemoryPath *node) +{ + ETreeMemoryPath *child, *next; + + child = node->first_child; + while (child) { + next = child->next_sibling; + child_free (etree, child); + child = next; + } + + if (etree->priv->destroy_func) { + etree->priv->destroy_func (node->node_data, etree->priv->destroy_user_data); + } + + g_slice_free (ETreeMemoryPath, node); +} + +/** + * e_tree_memory_node_remove: + * @etree: + * @path: + * + * + * + * Return value: + **/ +gpointer +e_tree_memory_node_remove (ETreeMemory *etree, + ETreePath node) +{ + ETreeMemoryPath *path = node; + ETreeMemoryPath *parent = path->parent; + ETreeMemoryPath *sibling; + gpointer ret = path->node_data; + gint old_position = 0; + + g_return_val_if_fail (etree != NULL, NULL); + + if (!etree->priv->frozen) { + e_tree_model_pre_change (E_TREE_MODEL (etree)); + for (old_position = 0, sibling = path; + sibling; + old_position++, sibling = sibling->prev_sibling) + /* Empty intentionally*/; + old_position--; + } + + /* unlink this node - we only have to unlink the root node being removed, + * since the others are only references from this node */ + e_tree_path_unlink (path); + + /*printf("removing %d nodes from position %d\n", visible, base);*/ + if (!etree->priv->frozen) + e_tree_model_node_removed (E_TREE_MODEL (etree), parent, path, old_position); + + child_free (etree, path); + + if (path == etree->priv->root) + etree->priv->root = NULL; + + if (!etree->priv->frozen) + e_tree_model_node_deleted (E_TREE_MODEL (etree), path); + + return ret; +} + +typedef struct { + ETreeMemory *memory; + gpointer closure; + ETreeMemorySortCallback callback; +} MemoryAndClosure; + +static gint +sort_callback (gconstpointer data1, + gconstpointer data2, + gpointer user_data) +{ + ETreePath path1 = *(ETreePath *) data1; + ETreePath path2 = *(ETreePath *) data2; + MemoryAndClosure *mac = user_data; + return (*mac->callback) (mac->memory, path1, path2, mac->closure); +} + +void +e_tree_memory_sort_node (ETreeMemory *etmm, + ETreePath node, + ETreeMemorySortCallback callback, + gpointer user_data) +{ + ETreeMemoryPath **children; + ETreeMemoryPath *child; + gint count; + gint i; + ETreeMemoryPath *path = node; + MemoryAndClosure mac; + ETreeMemoryPath *last; + + e_tree_model_pre_change (E_TREE_MODEL (etmm)); + + i = 0; + for (child = path->first_child; child; child = child->next_sibling) + i++; + + children = g_new (ETreeMemoryPath *, i); + + count = i; + + for (child = path->first_child, i = 0; + child; + child = child->next_sibling, i++) { + children[i] = child; + } + + mac.memory = etmm; + mac.closure = user_data; + mac.callback = callback; + + g_qsort_with_data ( + children, count, sizeof (ETreeMemoryPath *), + sort_callback, &mac); + + path->first_child = NULL; + last = NULL; + for (i = 0; + i < count; + i++) { + children[i]->prev_sibling = last; + if (last) + last->next_sibling = children[i]; + else + path->first_child = children[i]; + last = children[i]; + } + if (last) + last->next_sibling = NULL; + + path->last_child = last; + + g_free (children); + + e_tree_model_node_changed (E_TREE_MODEL (etmm), node); +} + +void +e_tree_memory_set_node_destroy_func (ETreeMemory *etmm, + GFunc destroy_func, + gpointer user_data) +{ + etmm->priv->destroy_func = destroy_func; + etmm->priv->destroy_user_data = user_data; +} diff --git a/e-util/e-tree-memory.h b/e-util/e-tree-memory.h new file mode 100644 index 0000000000..3e58952ad2 --- /dev/null +++ b/e-util/e-tree-memory.h @@ -0,0 +1,124 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Chris Toshok <toshok@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TREE_MEMORY_H_ +#define _E_TREE_MEMORY_H_ + +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include <e-util/e-tree-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TREE_MEMORY \ + (e_tree_memory_get_type ()) +#define E_TREE_MEMORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TREE_MEMORY, ETreeMemory)) +#define E_TREE_MEMORY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TREE_MEMORY, ETreeMemoryClass)) +#define E_IS_TREE_MEMORY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TREE_MEMORY)) +#define E_IS_TREE_MEMORY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TREE_MEMORY)) +#define E_TREE_MEMORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TREE_MEMORY, ETreeMemoryClass)) + +G_BEGIN_DECLS + +typedef struct _ETreeMemory ETreeMemory; +typedef struct _ETreeMemoryClass ETreeMemoryClass; +typedef struct _ETreeMemoryPrivate ETreeMemoryPrivate; + +typedef gint (*ETreeMemorySortCallback) (ETreeMemory *etmm, + ETreePath path1, + ETreePath path2, + gpointer closure); + +struct _ETreeMemory { + ETreeModel parent; + ETreeMemoryPrivate *priv; +}; + +struct _ETreeMemoryClass { + ETreeModelClass parent_class; + + /* Signals */ + void (*fill_in_children) (ETreeMemory *model, + ETreePath node); +}; + +GType e_tree_memory_get_type (void) G_GNUC_CONST; +void e_tree_memory_construct (ETreeMemory *etree); +ETreeMemory * e_tree_memory_new (void); + +/* node operations */ +ETreePath e_tree_memory_node_insert (ETreeMemory *etree, + ETreePath parent, + gint position, + gpointer node_data); +ETreePath e_tree_memory_node_insert_id (ETreeMemory *etree, + ETreePath parent, + gint position, + gpointer node_data, + gchar *id); +ETreePath e_tree_memory_node_insert_before + (ETreeMemory *etree, + ETreePath parent, + ETreePath sibling, + gpointer node_data); +gpointer e_tree_memory_node_remove (ETreeMemory *etree, + ETreePath path); + +/* Freeze and thaw */ +void e_tree_memory_freeze (ETreeMemory *etree); +void e_tree_memory_thaw (ETreeMemory *etree); +void e_tree_memory_set_expanded_default + (ETreeMemory *etree, + gboolean expanded); +gpointer e_tree_memory_node_get_data (ETreeMemory *etm, + ETreePath node); +void e_tree_memory_node_set_data (ETreeMemory *etm, + ETreePath node, + gpointer node_data); +void e_tree_memory_sort_node (ETreeMemory *etm, + ETreePath node, + ETreeMemorySortCallback callback, + gpointer user_data); +void e_tree_memory_set_node_destroy_func + (ETreeMemory *etmm, + GFunc destroy_func, + gpointer user_data); + +G_END_DECLS + +#endif /* _E_TREE_MEMORY_H */ + diff --git a/e-util/e-tree-model-generator.c b/e-util/e-tree-model-generator.c new file mode 100644 index 0000000000..aff912998c --- /dev/null +++ b/e-util/e-tree-model-generator.c @@ -0,0 +1,1345 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-tree-model-generator.c - Model wrapper that permutes underlying rows. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <string.h> +#include <glib/gi18n-lib.h> +#include "e-tree-model-generator.h" + +#define ETMG_DEBUG(x) + +#define ITER_IS_VALID(tree_model_generator, iter) \ + ((iter)->stamp == (tree_model_generator)->priv->stamp) +#define ITER_GET(iter, group, index) \ + G_STMT_START { \ + *(group) = (iter)->user_data; \ + *(index) = GPOINTER_TO_INT ((iter)->user_data2); \ + } G_STMT_END + +#define ITER_SET(tree_model_generator, iter, group, index) \ + G_STMT_START { \ + (iter)->stamp = (tree_model_generator)->priv->stamp; \ + (iter)->user_data = group; \ + (iter)->user_data2 = GINT_TO_POINTER (index); \ + } G_STMT_END + +#define E_TREE_MODEL_GENERATOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TREE_MODEL_GENERATOR, ETreeModelGeneratorPrivate)) + +struct _ETreeModelGeneratorPrivate { + GtkTreeModel *child_model; + GArray *root_nodes; + gint stamp; + + ETreeModelGeneratorGenerateFunc generate_func; + gpointer generate_func_data; + + ETreeModelGeneratorModifyFunc modify_func; + gpointer modify_func_data; +}; + +static void e_tree_model_generator_tree_model_init (GtkTreeModelIface *iface); + +G_DEFINE_TYPE_WITH_CODE ( + ETreeModelGenerator, e_tree_model_generator, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, e_tree_model_generator_tree_model_init)) + +static GtkTreeModelFlags e_tree_model_generator_get_flags (GtkTreeModel *tree_model); +static gint e_tree_model_generator_get_n_columns (GtkTreeModel *tree_model); +static GType e_tree_model_generator_get_column_type (GtkTreeModel *tree_model, + gint index); +static gboolean e_tree_model_generator_get_iter (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreePath *path); +static GtkTreePath *e_tree_model_generator_get_path (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static void e_tree_model_generator_get_value (GtkTreeModel *tree_model, + GtkTreeIter *iter, + gint column, + GValue *value); +static gboolean e_tree_model_generator_iter_next (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static gboolean e_tree_model_generator_iter_children (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent); +static gboolean e_tree_model_generator_iter_has_child (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static gint e_tree_model_generator_iter_n_children (GtkTreeModel *tree_model, + GtkTreeIter *iter); +static gboolean e_tree_model_generator_iter_nth_child (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent, + gint n); +static gboolean e_tree_model_generator_iter_parent (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *child); + +static GArray *build_node_map (ETreeModelGenerator *tree_model_generator, GtkTreeIter *parent_iter, + GArray *parent_group, gint parent_index); +static void release_node_map (GArray *group); + +static void child_row_changed (ETreeModelGenerator *tree_model_generator, GtkTreePath *path, GtkTreeIter *iter); +static void child_row_inserted (ETreeModelGenerator *tree_model_generator, GtkTreePath *path, GtkTreeIter *iter); +static void child_row_deleted (ETreeModelGenerator *tree_model_generator, GtkTreePath *path); + +typedef struct { + GArray *parent_group; + gint parent_index; + + gint n_generated; + GArray *child_nodes; +} +Node; + +enum { + PROP_0, + PROP_CHILD_MODEL +}; + +/* ------------------ * + * Class/object setup * + * ------------------ */ + +static void +tree_model_generator_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (object); + + switch (prop_id) + { + case PROP_CHILD_MODEL: + tree_model_generator->priv->child_model = g_value_get_object (value); + g_object_ref (tree_model_generator->priv->child_model); + + if (tree_model_generator->priv->root_nodes) + release_node_map (tree_model_generator->priv->root_nodes); + tree_model_generator->priv->root_nodes = + build_node_map (tree_model_generator, NULL, NULL, -1); + + g_signal_connect_swapped ( + tree_model_generator->priv->child_model, "row-changed", + G_CALLBACK (child_row_changed), tree_model_generator); + g_signal_connect_swapped ( + tree_model_generator->priv->child_model, "row-deleted", + G_CALLBACK (child_row_deleted), tree_model_generator); + g_signal_connect_swapped ( + tree_model_generator->priv->child_model, "row-inserted", + G_CALLBACK (child_row_inserted), tree_model_generator); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +tree_model_generator_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (object); + + switch (prop_id) + { + case PROP_CHILD_MODEL: + g_value_set_object (value, tree_model_generator->priv->child_model); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +tree_model_generator_finalize (GObject *object) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (object); + + if (tree_model_generator->priv->child_model) { + g_signal_handlers_disconnect_matched ( + tree_model_generator->priv->child_model, + G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, + tree_model_generator); + g_object_unref (tree_model_generator->priv->child_model); + } + + if (tree_model_generator->priv->root_nodes) + release_node_map (tree_model_generator->priv->root_nodes); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_tree_model_generator_parent_class)->finalize (object); +} + +static void +e_tree_model_generator_class_init (ETreeModelGeneratorClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ETreeModelGeneratorPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = tree_model_generator_get_property; + object_class->set_property = tree_model_generator_set_property; + object_class->finalize = tree_model_generator_finalize; + + g_object_class_install_property ( + object_class, + PROP_CHILD_MODEL, + g_param_spec_object ( + "child-model", + "Child Model", + "The child model to extend", + G_TYPE_OBJECT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); +} + +static void +e_tree_model_generator_tree_model_init (GtkTreeModelIface *iface) +{ + iface->get_flags = e_tree_model_generator_get_flags; + iface->get_n_columns = e_tree_model_generator_get_n_columns; + iface->get_column_type = e_tree_model_generator_get_column_type; + iface->get_iter = e_tree_model_generator_get_iter; + iface->get_path = e_tree_model_generator_get_path; + iface->get_value = e_tree_model_generator_get_value; + iface->iter_next = e_tree_model_generator_iter_next; + iface->iter_children = e_tree_model_generator_iter_children; + iface->iter_has_child = e_tree_model_generator_iter_has_child; + iface->iter_n_children = e_tree_model_generator_iter_n_children; + iface->iter_nth_child = e_tree_model_generator_iter_nth_child; + iface->iter_parent = e_tree_model_generator_iter_parent; +} + +static void +e_tree_model_generator_init (ETreeModelGenerator *tree_model_generator) +{ + tree_model_generator->priv = + E_TREE_MODEL_GENERATOR_GET_PRIVATE (tree_model_generator); + + tree_model_generator->priv->stamp = g_random_int (); + tree_model_generator->priv->root_nodes = g_array_new (FALSE, FALSE, sizeof (Node)); +} + +/* ------------------ * + * Row update helpers * + * ------------------ */ + +static void +row_deleted (ETreeModelGenerator *tree_model_generator, + GtkTreePath *path) +{ + g_assert (path); + + ETMG_DEBUG (g_print ("row_deleted emitting\n")); + gtk_tree_model_row_deleted (GTK_TREE_MODEL (tree_model_generator), path); +} + +static void +row_inserted (ETreeModelGenerator *tree_model_generator, + GtkTreePath *path) +{ + GtkTreeIter iter; + + g_assert (path); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (tree_model_generator), &iter, path)) { + ETMG_DEBUG (g_print ("row_inserted emitting\n")); + gtk_tree_model_row_inserted (GTK_TREE_MODEL (tree_model_generator), path, &iter); + } else { + ETMG_DEBUG (g_print ("row_inserted could not get iter!\n")); + } +} + +static void +row_changed (ETreeModelGenerator *tree_model_generator, + GtkTreePath *path) +{ + GtkTreeIter iter; + + g_assert (path); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (tree_model_generator), &iter, path)) { + ETMG_DEBUG (g_print ("row_changed emitting\n")); + gtk_tree_model_row_changed (GTK_TREE_MODEL (tree_model_generator), path, &iter); + } else { + ETMG_DEBUG (g_print ("row_changed could not get iter!\n")); + } +} + +/* -------------------- * + * Node map translation * + * -------------------- */ + +static gint +generated_offset_to_child_offset (GArray *group, + gint offset, + gint *internal_offset) +{ + gboolean success = FALSE; + gint accum_offset = 0; + gint i; + + for (i = 0; i < group->len; i++) { + Node *node = &g_array_index (group, Node, i); + + accum_offset += node->n_generated; + if (accum_offset > offset) { + accum_offset -= node->n_generated; + success = TRUE; + break; + } + } + + if (!success) + return -1; + + if (internal_offset) + *internal_offset = offset - accum_offset; + + return i; +} + +static gint +child_offset_to_generated_offset (GArray *group, + gint offset) +{ + gint accum_offset = 0; + gint i; + + g_return_val_if_fail (group != NULL, -1); + + for (i = 0; i < group->len && i < offset; i++) { + Node *node = &g_array_index (group, Node, i); + + accum_offset += node->n_generated; + } + + return accum_offset; +} + +static gint +count_generated_nodes (GArray *group) +{ + gint accum_offset = 0; + gint i; + + for (i = 0; i < group->len; i++) { + Node *node = &g_array_index (group, Node, i); + + accum_offset += node->n_generated; + } + + return accum_offset; +} + +/* ------------------- * + * Node map management * + * ------------------- */ + +static void +release_node_map (GArray *group) +{ + gint i; + + for (i = 0; i < group->len; i++) { + Node *node = &g_array_index (group, Node, i); + + if (node->child_nodes) + release_node_map (node->child_nodes); + } + + g_array_free (group, TRUE); +} + +static gint +append_node (GArray *group) +{ + g_array_set_size (group, group->len + 1); + return group->len - 1; +} + +static GArray * +build_node_map (ETreeModelGenerator *tree_model_generator, + GtkTreeIter *parent_iter, + GArray *parent_group, + gint parent_index) +{ + GArray *group; + GtkTreeIter iter; + gboolean result; + + if (parent_iter) + result = gtk_tree_model_iter_children (tree_model_generator->priv->child_model, &iter, parent_iter); + else + result = gtk_tree_model_get_iter_first (tree_model_generator->priv->child_model, &iter); + + if (!result) + return NULL; + + group = g_array_new (FALSE, FALSE, sizeof (Node)); + + do { + Node *node; + gint i; + + i = append_node (group); + node = &g_array_index (group, Node, i); + + node->parent_group = parent_group; + node->parent_index = parent_index; + + if (tree_model_generator->priv->generate_func) + node->n_generated = + tree_model_generator->priv->generate_func (tree_model_generator->priv->child_model, + &iter, tree_model_generator->priv->generate_func_data); + else + node->n_generated = 1; + + node->child_nodes = build_node_map (tree_model_generator, &iter, group, i); + } while (gtk_tree_model_iter_next (tree_model_generator->priv->child_model, &iter)); + + return group; +} + +static gint +get_first_visible_index_from (GArray *group, + gint index) +{ + gint i; + + for (i = index; i < group->len; i++) { + Node *node = &g_array_index (group, Node, i); + + if (node->n_generated) + break; + } + + if (i >= group->len) + i = -1; + + return i; +} + +static Node * +get_node_by_child_path (ETreeModelGenerator *tree_model_generator, + GtkTreePath *path, + GArray **node_group) +{ + Node *node = NULL; + GArray *group; + gint depth; + + group = tree_model_generator->priv->root_nodes; + + for (depth = 0; depth < gtk_tree_path_get_depth (path); depth++) { + gint index; + + if (!group) { + g_warning ("ETreeModelGenerator got unknown child element!"); + break; + } + + index = gtk_tree_path_get_indices (path)[depth]; + node = &g_array_index (group, Node, index); + + if (depth + 1 < gtk_tree_path_get_depth (path)) + group = node->child_nodes; + } + + if (!node) + group = NULL; + + if (node_group) + *node_group = group; + + return node; +} + +static Node * +create_node_at_child_path (ETreeModelGenerator *tree_model_generator, + GtkTreePath *path) +{ + GtkTreePath *parent_path; + gint parent_index; + GArray *parent_group; + GArray *group; + gint index; + Node *node; + + parent_path = gtk_tree_path_copy (path); + gtk_tree_path_up (parent_path); + node = get_node_by_child_path (tree_model_generator, parent_path, &parent_group); + + if (node) { + if (!node->child_nodes) + node->child_nodes = g_array_new (FALSE, FALSE, sizeof (Node)); + + group = node->child_nodes; + parent_index = gtk_tree_path_get_indices (parent_path)[gtk_tree_path_get_depth (parent_path) - 1]; + } else { + if (!tree_model_generator->priv->root_nodes) + tree_model_generator->priv->root_nodes = g_array_new (FALSE, FALSE, sizeof (Node)); + + group = tree_model_generator->priv->root_nodes; + parent_index = -1; + } + + gtk_tree_path_free (parent_path); + + index = gtk_tree_path_get_indices (path)[gtk_tree_path_get_depth (path) - 1]; + ETMG_DEBUG (g_print ("Inserting index %d into group of length %d\n", index, group->len)); + index = MIN (index, group->len); + + append_node (group); + + if (group->len - 1 - index > 0) { + gint i; + + memmove ( + (Node *) group->data + index + 1, + (Node *) group->data + index, + (group->len - 1 - index) * sizeof (Node)); + + /* Update parent pointers */ + for (i = index + 1; i < group->len; i++) { + Node *pnode = &g_array_index (group, Node, i); + GArray *child_group; + gint j; + + child_group = pnode->child_nodes; + if (!child_group) + continue; + + for (j = 0; j < child_group->len; j++) { + Node *child_node = &g_array_index (child_group, Node, j); + child_node->parent_index = i; + } + } + } + + node = &g_array_index (group, Node, index); + node->parent_group = parent_group; + node->parent_index = parent_index; + node->n_generated = 0; + node->child_nodes = NULL; + + ETMG_DEBUG ( + g_print ("Created node at offset %d, parent_group = %p, parent_index = %d\n", + index, node->parent_group, node->parent_index)); + + return node; +} + +ETMG_DEBUG ( + +static void +dump_group (GArray *group) +{ + gint i; + + g_print ("\nGroup %p:\n", group); + + for (i = 0; i < group->len; i++) { + Node *node = &g_array_index (group, Node, i); + g_print ( + " %04d: pgroup=%p, pindex=%d, n_generated=%d, child_nodes=%p\n", + i, node->parent_group, node->parent_index, node->n_generated, node->child_nodes); + } +} + +) + +static void +delete_node_at_child_path (ETreeModelGenerator *tree_model_generator, + GtkTreePath *path) +{ + GtkTreePath *parent_path; + GArray *parent_group; + GArray *group; + gint index; + Node *node; + gint i; + + parent_path = gtk_tree_path_copy (path); + gtk_tree_path_up (parent_path); + node = get_node_by_child_path (tree_model_generator, parent_path, &parent_group); + + if (node) { + group = node->child_nodes; + } else { + group = tree_model_generator->priv->root_nodes; + } + + gtk_tree_path_free (parent_path); + + if (!group) + return; + + index = gtk_tree_path_get_indices (path)[gtk_tree_path_get_depth (path) - 1]; + if (index >= group->len) + return; + + node = &g_array_index (group, Node, index); + if (node->child_nodes) + release_node_map (node->child_nodes); + g_array_remove_index (group, index); + + /* Update parent pointers */ + for (i = index; i < group->len; i++) { + Node *pnode = &g_array_index (group, Node, i); + GArray *child_group; + gint j; + + child_group = pnode->child_nodes; + if (!child_group) + continue; + + for (j = 0; j < child_group->len; j++) { + Node *child_node = &g_array_index (child_group, Node, j); + child_node->parent_index = i; + } + } +} + +static void +child_row_changed (ETreeModelGenerator *tree_model_generator, + GtkTreePath *path, + GtkTreeIter *iter) +{ + GtkTreePath *generated_path; + Node *node; + gint n_generated; + gint i; + + if (tree_model_generator->priv->generate_func) + n_generated = + tree_model_generator->priv->generate_func (tree_model_generator->priv->child_model, + iter, tree_model_generator->priv->generate_func_data); + else + n_generated = 1; + + node = get_node_by_child_path (tree_model_generator, path, NULL); + if (!node) + return; + + generated_path = e_tree_model_generator_convert_child_path_to_path (tree_model_generator, path); + + /* FIXME: Converting the path to an iter every time is inefficient */ + + for (i = 0; i < n_generated && i < node->n_generated; i++) { + row_changed (tree_model_generator, generated_path); + gtk_tree_path_next (generated_path); + } + + for (; i < node->n_generated; ) { + node->n_generated--; + row_deleted (tree_model_generator, generated_path); + } + + for (; i < n_generated; i++) { + node->n_generated++; + row_inserted (tree_model_generator, generated_path); + gtk_tree_path_next (generated_path); + } + + gtk_tree_path_free (generated_path); +} + +static void +child_row_inserted (ETreeModelGenerator *tree_model_generator, + GtkTreePath *path, + GtkTreeIter *iter) +{ + GtkTreePath *generated_path; + Node *node; + gint n_generated; + + if (tree_model_generator->priv->generate_func) + n_generated = + tree_model_generator->priv->generate_func (tree_model_generator->priv->child_model, + iter, tree_model_generator->priv->generate_func_data); + else + n_generated = 1; + + node = create_node_at_child_path (tree_model_generator, path); + if (!node) + return; + + generated_path = e_tree_model_generator_convert_child_path_to_path (tree_model_generator, path); + + /* FIXME: Converting the path to an iter every time is inefficient */ + + for (node->n_generated = 0; node->n_generated < n_generated; ) { + node->n_generated++; + row_inserted (tree_model_generator, generated_path); + gtk_tree_path_next (generated_path); + } + + gtk_tree_path_free (generated_path); +} + +static void +child_row_deleted (ETreeModelGenerator *tree_model_generator, + GtkTreePath *path) +{ + GtkTreePath *generated_path; + Node *node; + + node = get_node_by_child_path (tree_model_generator, path, NULL); + if (!node) + return; + + generated_path = e_tree_model_generator_convert_child_path_to_path (tree_model_generator, path); + + /* FIXME: Converting the path to an iter every time is inefficient */ + + for (; node->n_generated; ) { + node->n_generated--; + row_deleted (tree_model_generator, generated_path); + } + + delete_node_at_child_path (tree_model_generator, path); + gtk_tree_path_free (generated_path); +} + +/* ----------------------- * + * ETreeModelGenerator API * + * ----------------------- */ + +/** + * e_tree_model_generator_new: + * @child_model: a #GtkTreeModel + * + * Creates a new #ETreeModelGenerator wrapping @child_model. + * + * Returns: A new #ETreeModelGenerator. + **/ +ETreeModelGenerator * +e_tree_model_generator_new (GtkTreeModel *child_model) +{ + g_return_val_if_fail (GTK_IS_TREE_MODEL (child_model), NULL); + + return E_TREE_MODEL_GENERATOR ( + g_object_new (E_TYPE_TREE_MODEL_GENERATOR, + "child-model", child_model, NULL)); +} + +/** + * e_tree_model_generator_get_model: + * @tree_model_generator: an #ETreeModelGenerator + * + * Gets the child model being wrapped by @tree_model_generator. + * + * Returns: A #GtkTreeModel being wrapped. + **/ +GtkTreeModel * +e_tree_model_generator_get_model (ETreeModelGenerator *tree_model_generator) +{ + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model_generator), NULL); + + return tree_model_generator->priv->child_model; +} + +/** + * e_tree_model_generator_set_generate_func: + * @tree_model_generator: an #ETreeModelGenerator + * @func: an #ETreeModelGeneratorGenerateFunc, or %NULL + * @data: user data to pass to @func + * @destroy: + * + * Sets the callback function used to filter or generate additional rows + * based on the child model's data. This function is called for each child + * row, and returns a value indicating the number of rows that will be + * used to represent the child row - 0 or more. + * + * If @func is %NULL, a filtering/generating function will not be applied. + **/ +void +e_tree_model_generator_set_generate_func (ETreeModelGenerator *tree_model_generator, + ETreeModelGeneratorGenerateFunc func, + gpointer data, + GDestroyNotify destroy) +{ + g_return_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model_generator)); + + tree_model_generator->priv->generate_func = func; + tree_model_generator->priv->generate_func_data = data; +} + +/** + * e_tree_model_generator_set_modify_func: + * @tree_model_generator: an #ETreeModelGenerator + * @func: an @ETreeModelGeneratorModifyFunc, or %NULL + * @data: user data to pass to @func + * @destroy: + * + * Sets the callback function used to override values for the child row's + * columns and specify values for generated rows' columns. + * + * If @func is %NULL, the child model's values will always be used. + **/ +void +e_tree_model_generator_set_modify_func (ETreeModelGenerator *tree_model_generator, + ETreeModelGeneratorModifyFunc func, + gpointer data, + GDestroyNotify destroy) +{ + g_return_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model_generator)); + + tree_model_generator->priv->modify_func = func; + tree_model_generator->priv->modify_func_data = data; +} + +/** + * e_tree_model_generator_convert_child_path_to_path: + * @tree_model_generator: an #ETreeModelGenerator + * @child_path: a #GtkTreePath + * + * Convert a path to a child row to a path to a @tree_model_generator row. + * + * Returns: A new GtkTreePath, owned by the caller. + **/ +GtkTreePath * +e_tree_model_generator_convert_child_path_to_path (ETreeModelGenerator *tree_model_generator, + GtkTreePath *child_path) +{ + GtkTreePath *path; + GArray *group; + gint depth; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model_generator), NULL); + g_return_val_if_fail (child_path != NULL, NULL); + + path = gtk_tree_path_new (); + + group = tree_model_generator->priv->root_nodes; + + for (depth = 0; depth < gtk_tree_path_get_depth (child_path); depth++) { + Node *node; + gint index; + gint generated_index; + + if (!group) { + g_warning ("ETreeModelGenerator was asked for path to unknown child element!"); + break; + } + + index = gtk_tree_path_get_indices (child_path)[depth]; + generated_index = child_offset_to_generated_offset (group, index); + node = &g_array_index (group, Node, index); + group = node->child_nodes; + + gtk_tree_path_append_index (path, generated_index); + } + + return path; +} + +/** + * e_tree_model_generator_convert_child_iter_to_iter: + * @tree_model_generator: an #ETreeModelGenerator + * @generator_iter: a #GtkTreeIter to set + * @child_iter: a #GtkTreeIter to convert + * + * Convert @child_iter to a corresponding #GtkTreeIter for @tree_model_generator, + * storing the result in @generator_iter. + **/ +void +e_tree_model_generator_convert_child_iter_to_iter (ETreeModelGenerator *tree_model_generator, + GtkTreeIter *generator_iter, + GtkTreeIter *child_iter) +{ + GtkTreePath *path; + GArray *group; + gint depth; + gint index = 0; + + g_return_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model_generator)); + + path = gtk_tree_model_get_path (tree_model_generator->priv->child_model, child_iter); + if (!path) + return; + + group = tree_model_generator->priv->root_nodes; + + for (depth = 0; depth < gtk_tree_path_get_depth (path); depth++) { + Node *node; + + index = gtk_tree_path_get_indices (path)[depth]; + node = &g_array_index (group, Node, index); + + if (depth + 1 < gtk_tree_path_get_depth (path)) + group = node->child_nodes; + + if (!group) { + g_warning ("ETreeModelGenerator was asked for iter to unknown child element!"); + break; + } + } + + g_return_if_fail (group != NULL); + + index = child_offset_to_generated_offset (group, index); + ITER_SET (tree_model_generator, generator_iter, group, index); + gtk_tree_path_free (path); +} + +/** + * e_tree_model_generator_convert_path_to_child_path: + * @tree_model_generator: an #ETreeModelGenerator + * @generator_path: a #GtkTreePath to a @tree_model_generator row + * + * Converts @generator_path to a corresponding #GtkTreePath in the child model. + * + * Returns: A new #GtkTreePath, owned by the caller. + **/ +GtkTreePath * +e_tree_model_generator_convert_path_to_child_path (ETreeModelGenerator *tree_model_generator, + GtkTreePath *generator_path) +{ + GtkTreePath *path; + GArray *group; + gint depth; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model_generator), NULL); + g_return_val_if_fail (generator_path != NULL, NULL); + + path = gtk_tree_path_new (); + + group = tree_model_generator->priv->root_nodes; + + for (depth = 0; depth < gtk_tree_path_get_depth (generator_path); depth++) { + Node *node; + gint index; + gint child_index; + + if (!group) { + g_warning ("ETreeModelGenerator was asked for path to unknown child element!"); + break; + } + + index = gtk_tree_path_get_indices (generator_path)[depth]; + child_index = generated_offset_to_child_offset (group, index, NULL); + node = &g_array_index (group, Node, child_index); + group = node->child_nodes; + + gtk_tree_path_append_index (path, child_index); + } + + return path; +} + +/** + * e_tree_model_generator_convert_iter_to_child_iter: + * @tree_model_generator: an #ETreeModelGenerator + * @child_iter: a #GtkTreeIter to set + * @permutation_n: a permutation index to set + * @generator_iter: a #GtkTreeIter indicating the row to convert + * + * Converts a @tree_model_generator row into a child row and permutation index. + * The permutation index is the index of the generated row based on this + * child row, with the first generated row based on this child row being 0. + **/ +void +e_tree_model_generator_convert_iter_to_child_iter (ETreeModelGenerator *tree_model_generator, + GtkTreeIter *child_iter, + gint *permutation_n, + GtkTreeIter *generator_iter) +{ + GtkTreePath *path; + GArray *group; + gint index; + gint internal_offset = 0; + + g_return_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model_generator)); + g_return_if_fail (ITER_IS_VALID (tree_model_generator, generator_iter)); + + path = gtk_tree_path_new (); + ITER_GET (generator_iter, &group, &index); + + index = generated_offset_to_child_offset (group, index, &internal_offset); + gtk_tree_path_prepend_index (path, index); + + while (group) { + Node *node = &g_array_index (group, Node, index); + + group = node->parent_group; + index = node->parent_index; + + if (group) + gtk_tree_path_prepend_index (path, index); + } + + if (child_iter) + gtk_tree_model_get_iter (tree_model_generator->priv->child_model, child_iter, path); + if (permutation_n) + *permutation_n = internal_offset; + + gtk_tree_path_free (path); +} + +/* ---------------- * + * GtkTreeModel API * + * ---------------- */ + +static GtkTreeModelFlags +e_tree_model_generator_get_flags (GtkTreeModel *tree_model) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), 0); + + return gtk_tree_model_get_flags (tree_model_generator->priv->child_model); +} + +static gint +e_tree_model_generator_get_n_columns (GtkTreeModel *tree_model) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), 0); + + return gtk_tree_model_get_n_columns (tree_model_generator->priv->child_model); +} + +static GType +e_tree_model_generator_get_column_type (GtkTreeModel *tree_model, + gint index) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), G_TYPE_INVALID); + + return gtk_tree_model_get_column_type (tree_model_generator->priv->child_model, index); +} + +static gboolean +e_tree_model_generator_get_iter (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreePath *path) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + GArray *group; + gint depth; + gint index = 0; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), FALSE); + g_return_val_if_fail (gtk_tree_path_get_depth (path) > 0, FALSE); + + group = tree_model_generator->priv->root_nodes; + if (!group) + return FALSE; + + for (depth = 0; depth < gtk_tree_path_get_depth (path); depth++) { + Node *node; + gint child_index; + + index = gtk_tree_path_get_indices (path)[depth]; + child_index = generated_offset_to_child_offset (group, index, NULL); + if (child_index < 0) + return FALSE; + + node = &g_array_index (group, Node, child_index); + + if (depth + 1 < gtk_tree_path_get_depth (path)) { + group = node->child_nodes; + if (!group) + return FALSE; + } + } + + ITER_SET (tree_model_generator, iter, group, index); + return TRUE; +} + +static GtkTreePath * +e_tree_model_generator_get_path (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + GtkTreePath *path; + GArray *group; + gint index; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), NULL); + g_return_val_if_fail (ITER_IS_VALID (tree_model_generator, iter), NULL); + + ITER_GET (iter, &group, &index); + path = gtk_tree_path_new (); + + /* FIXME: Converting a path to an iter is a destructive operation, because + * we don't store a node for each generated entry... Doesn't matter for + * lists, not sure about trees. */ + + gtk_tree_path_prepend_index (path, index); + index = generated_offset_to_child_offset (group, index, NULL); + + while (group) { + Node *node = &g_array_index (group, Node, index); + gint generated_index; + + group = node->parent_group; + index = node->parent_index; + if (group) { + generated_index = child_offset_to_generated_offset (group, index); + gtk_tree_path_prepend_index (path, generated_index); + } + } + + return path; +} + +static gboolean +e_tree_model_generator_iter_next (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + Node *node; + GArray *group; + gint index; + gint child_index; + gint internal_offset = 0; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), FALSE); + g_return_val_if_fail (ITER_IS_VALID (tree_model_generator, iter), FALSE); + + ITER_GET (iter, &group, &index); + child_index = generated_offset_to_child_offset (group, index, &internal_offset); + node = &g_array_index (group, Node, child_index); + + if (internal_offset + 1 < node->n_generated || + get_first_visible_index_from (group, child_index + 1) >= 0) { + ITER_SET (tree_model_generator, iter, group, index + 1); + return TRUE; + } + + return FALSE; +} + +static gboolean +e_tree_model_generator_iter_children (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + Node *node; + GArray *group; + gint index; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), FALSE); + + if (!parent) { + if (!tree_model_generator->priv->root_nodes || + !count_generated_nodes (tree_model_generator->priv->root_nodes)) + return FALSE; + + ITER_SET (tree_model_generator, iter, tree_model_generator->priv->root_nodes, 0); + return TRUE; + } + + ITER_GET (parent, &group, &index); + index = generated_offset_to_child_offset (group, index, NULL); + if (index < 0) + return FALSE; + + node = &g_array_index (group, Node, index); + + if (!node->child_nodes) + return FALSE; + + if (!count_generated_nodes (node->child_nodes)) + return FALSE; + + ITER_SET (tree_model_generator, iter, node->child_nodes, 0); + return TRUE; +} + +static gboolean +e_tree_model_generator_iter_has_child (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + Node *node; + GArray *group; + gint index; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), FALSE); + + if (iter == NULL) { + if (!tree_model_generator->priv->root_nodes || + !count_generated_nodes (tree_model_generator->priv->root_nodes)) + return FALSE; + + return TRUE; + } + + ITER_GET (iter, &group, &index); + index = generated_offset_to_child_offset (group, index, NULL); + if (index < 0) + return FALSE; + + node = &g_array_index (group, Node, index); + + if (!node->child_nodes) + return FALSE; + + if (!count_generated_nodes (node->child_nodes)) + return FALSE; + + return TRUE; +} + +static gint +e_tree_model_generator_iter_n_children (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + Node *node; + GArray *group; + gint index; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), 0); + + if (iter == NULL) + return tree_model_generator->priv->root_nodes ? + count_generated_nodes (tree_model_generator->priv->root_nodes) : 0; + + ITER_GET (iter, &group, &index); + index = generated_offset_to_child_offset (group, index, NULL); + if (index < 0) + return 0; + + node = &g_array_index (group, Node, index); + + if (!node->child_nodes) + return 0; + + return count_generated_nodes (node->child_nodes); +} + +static gboolean +e_tree_model_generator_iter_nth_child (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent, + gint n) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + Node *node; + GArray *group; + gint index; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), FALSE); + + if (!parent) { + if (!tree_model_generator->priv->root_nodes) + return FALSE; + + if (n >= count_generated_nodes (tree_model_generator->priv->root_nodes)) + return FALSE; + + ITER_SET (tree_model_generator, iter, tree_model_generator->priv->root_nodes, n); + return TRUE; + } + + ITER_GET (parent, &group, &index); + index = generated_offset_to_child_offset (group, index, NULL); + if (index < 0) + return FALSE; + + node = &g_array_index (group, Node, index); + + if (!node->child_nodes) + return FALSE; + + if (n >= count_generated_nodes (node->child_nodes)) + return FALSE; + + ITER_SET (tree_model_generator, iter, node->child_nodes, n); + return TRUE; +} + +static gboolean +e_tree_model_generator_iter_parent (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *child) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + Node *node; + GArray *group; + gint index; + + g_return_val_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model), FALSE); + g_return_val_if_fail (ITER_IS_VALID (tree_model_generator, iter), FALSE); + + ITER_GET (child, &group, &index); + index = generated_offset_to_child_offset (group, index, NULL); + if (index < 0) + return FALSE; + + node = &g_array_index (group, Node, index); + + group = node->parent_group; + if (!group) + return FALSE; + + ITER_SET (tree_model_generator, iter, group, node->parent_index); + return TRUE; +} + +static void +e_tree_model_generator_get_value (GtkTreeModel *tree_model, + GtkTreeIter *iter, + gint column, + GValue *value) +{ + ETreeModelGenerator *tree_model_generator = E_TREE_MODEL_GENERATOR (tree_model); + GtkTreeIter child_iter; + gint permutation_n; + + g_return_if_fail (E_IS_TREE_MODEL_GENERATOR (tree_model)); + g_return_if_fail (ITER_IS_VALID (tree_model_generator, iter)); + + e_tree_model_generator_convert_iter_to_child_iter ( + tree_model_generator, &child_iter, + &permutation_n, iter); + + if (tree_model_generator->priv->modify_func) { + tree_model_generator->priv->modify_func (tree_model_generator->priv->child_model, + &child_iter, permutation_n, + column, value, + tree_model_generator->priv->modify_func_data); + return; + } + + gtk_tree_model_get_value (tree_model_generator->priv->child_model, &child_iter, column, value); +} diff --git a/e-util/e-tree-model-generator.h b/e-util/e-tree-model-generator.h new file mode 100644 index 0000000000..e85a1adc12 --- /dev/null +++ b/e-util/e-tree-model-generator.h @@ -0,0 +1,104 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* e-tree-model-generator.h - Model wrapper that permutes underlying rows. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Authors: Hans Petter Jansson <hpj@novell.com> + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_TREE_MODEL_GENERATOR_H +#define E_TREE_MODEL_GENERATOR_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_TREE_MODEL_GENERATOR \ + (e_tree_model_generator_get_type ()) +#define E_TREE_MODEL_GENERATOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TREE_MODEL_GENERATOR, ETreeModelGenerator)) +#define E_TREE_MODEL_GENERATOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TREE_MODEL_GENERATOR, ETreeModelGeneratorClass)) +#define E_IS_TREE_MODEL_GENERATOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TREE_MODEL_GENERATOR)) +#define E_IS_TREE_MODEL_GENERATOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TREE_MODEL_GENERATOR)) +#define E_TREE_MODEL_GENERATOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TREE_MODEL_GENERATOR, ETreeModelGeneratorClass)) + +G_BEGIN_DECLS + +typedef gint (*ETreeModelGeneratorGenerateFunc) (GtkTreeModel *model, GtkTreeIter *child_iter, + gpointer data); +typedef void (*ETreeModelGeneratorModifyFunc) (GtkTreeModel *model, GtkTreeIter *child_iter, + gint permutation_n, gint column, GValue *value, + gpointer data); + +typedef struct _ETreeModelGenerator ETreeModelGenerator; +typedef struct _ETreeModelGeneratorClass ETreeModelGeneratorClass; +typedef struct _ETreeModelGeneratorPrivate ETreeModelGeneratorPrivate; + +struct _ETreeModelGenerator { + GObject parent; + ETreeModelGeneratorPrivate *priv; +}; + +struct _ETreeModelGeneratorClass { + GObjectClass parent_class; +}; + +GType e_tree_model_generator_get_type (void); +ETreeModelGenerator * + e_tree_model_generator_new (GtkTreeModel *child_model); +GtkTreeModel * e_tree_model_generator_get_model (ETreeModelGenerator *tree_model_generator); +void e_tree_model_generator_set_generate_func + (ETreeModelGenerator *tree_model_generator, + ETreeModelGeneratorGenerateFunc func, + gpointer data, + GDestroyNotify destroy); +void e_tree_model_generator_set_modify_func + (ETreeModelGenerator *tree_model_generator, + ETreeModelGeneratorModifyFunc func, + gpointer data, + GDestroyNotify destroy); +GtkTreePath * e_tree_model_generator_convert_child_path_to_path + (ETreeModelGenerator *tree_model_generator, + GtkTreePath *child_path); +void e_tree_model_generator_convert_child_iter_to_iter + (ETreeModelGenerator *tree_model_generator, + GtkTreeIter *generator_iter, + GtkTreeIter *child_iter); +GtkTreePath * e_tree_model_generator_convert_path_to_child_path + (ETreeModelGenerator *tree_model_generator, + GtkTreePath *generator_path); +void e_tree_model_generator_convert_iter_to_child_iter + (ETreeModelGenerator *tree_model_generator, + GtkTreeIter *child_iter, + gint *permutation_n, + GtkTreeIter *generator_iter); + +G_END_DECLS + +#endif /* E_TREE_MODEL_GENERATOR_H */ diff --git a/e-util/e-tree-model.c b/e-util/e-tree-model.c new file mode 100644 index 0000000000..db763cf782 --- /dev/null +++ b/e-util/e-tree-model.c @@ -0,0 +1,1177 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Chris Toshok <toshok@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-tree-model.h" + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> + +#include <gtk/gtk.h> +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include "e-marshal.h" +#include "e-xml-utils.h" + +#define ETM_CLASS(e) (E_TREE_MODEL_GET_CLASS(e)) + +#define d(x) + +G_DEFINE_TYPE (ETreeModel, e_tree_model, G_TYPE_OBJECT) + +enum { + PRE_CHANGE, + NO_CHANGE, + NODE_CHANGED, + NODE_DATA_CHANGED, + NODE_COL_CHANGED, + NODE_INSERTED, + NODE_REMOVED, + NODE_DELETED, + NODE_REQUEST_COLLAPSE, + REBUILT, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0, }; + +static void +e_tree_model_class_init (ETreeModelClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + signals[PRE_CHANGE] = g_signal_new ( + "pre_change", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, pre_change), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[NO_CHANGE] = g_signal_new ( + "no_change", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, no_change), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[REBUILT] = g_signal_new ( + "rebuilt", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, rebuilt), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[NODE_CHANGED] = g_signal_new ( + "node_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, node_changed), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + signals[NODE_DATA_CHANGED] = g_signal_new ( + "node_data_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, node_data_changed), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + signals[NODE_COL_CHANGED] = g_signal_new ( + "node_col_changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, node_col_changed), + (GSignalAccumulator) NULL, NULL, + e_marshal_VOID__POINTER_INT, + G_TYPE_NONE, 2, + G_TYPE_POINTER, + G_TYPE_INT); + + signals[NODE_INSERTED] = g_signal_new ( + "node_inserted", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, node_inserted), + (GSignalAccumulator) NULL, NULL, + e_marshal_VOID__POINTER_POINTER, + G_TYPE_NONE, 2, + G_TYPE_POINTER, + G_TYPE_POINTER); + + signals[NODE_REMOVED] = g_signal_new ( + "node_removed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, node_removed), + (GSignalAccumulator) NULL, NULL, + e_marshal_VOID__POINTER_POINTER_INT, + G_TYPE_NONE, 3, + G_TYPE_POINTER, + G_TYPE_POINTER, + G_TYPE_INT); + + signals[NODE_DELETED] = g_signal_new ( + "node_deleted", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, node_deleted), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + signals[NODE_REQUEST_COLLAPSE] = g_signal_new ( + "node_request_collapse", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeModelClass, node_request_collapse), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + class->get_root = NULL; + + class->get_parent = NULL; + class->get_first_child = NULL; + class->get_last_child = NULL; + class->get_next = NULL; + class->get_prev = NULL; + + class->is_root = NULL; + class->is_expandable = NULL; + class->get_children = NULL; + class->depth = NULL; + + class->icon_at = NULL; + + class->get_expanded_default = NULL; + class->column_count = NULL; + + class->has_save_id = NULL; + class->get_save_id = NULL; + class->has_get_node_by_id = NULL; + class->get_node_by_id = NULL; + + class->has_change_pending = NULL; + + class->sort_value_at = NULL; + class->value_at = NULL; + class->set_value_at = NULL; + class->is_editable = NULL; + + class->duplicate_value = NULL; + class->free_value = NULL; + class->initialize_value = NULL; + class->value_is_empty = NULL; + class->value_to_string = NULL; + + class->pre_change = NULL; + class->no_change = NULL; + class->rebuilt = NULL; + class->node_changed = NULL; + class->node_data_changed = NULL; + class->node_col_changed = NULL; + class->node_inserted = NULL; + class->node_removed = NULL; + class->node_deleted = NULL; + class->node_request_collapse = NULL; +} + +static void +e_tree_model_init (ETreeModel *tree_model) +{ + /* nothing to do */ +} + +/* signals */ + +/** + * e_tree_model_node_changed: + * @tree_model: + * @node: + * + * + * + * Return value: + **/ +void +e_tree_model_pre_change (ETreeModel *tree_model) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit (tree_model, signals[PRE_CHANGE], 0); +} + +/** + * e_tree_model_node_changed: + * @tree_model: + * @node: + * + * + * + * Return value: + **/ +void +e_tree_model_no_change (ETreeModel *tree_model) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit (tree_model, signals[NO_CHANGE], 0); +} + +/** + * e_tree_model_rebuilt: + * @tree_model: + * @node: + * + * + * + * Return value: + **/ +void +e_tree_model_rebuilt (ETreeModel *tree_model) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit (tree_model, signals[REBUILT], 0); +} +/** + * e_tree_model_node_changed: + * @tree_model: + * @node: + * + * + * + * Return value: + **/ +void +e_tree_model_node_changed (ETreeModel *tree_model, + ETreePath node) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit (tree_model, signals[NODE_CHANGED], 0, node); +} + +/** + * e_tree_model_node_data_changed: + * @tree_model: + * @node: + * + * + * + * Return value: + **/ +void +e_tree_model_node_data_changed (ETreeModel *tree_model, + ETreePath node) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit (tree_model, signals[NODE_DATA_CHANGED], 0, node); +} + +/** + * e_tree_model_node_col_changed: + * @tree_model: + * @node: + * + * + * + * Return value: + **/ +void +e_tree_model_node_col_changed (ETreeModel *tree_model, + ETreePath node, + gint col) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit (tree_model, signals[NODE_COL_CHANGED], 0, node, col); +} + +/** + * e_tree_model_node_inserted: + * @tree_model: + * @parent_node: + * @inserted_node: + * + * + **/ +void +e_tree_model_node_inserted (ETreeModel *tree_model, + ETreePath parent_node, + ETreePath inserted_node) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit ( + tree_model, signals[NODE_INSERTED], 0, + parent_node, inserted_node); +} + +/** + * e_tree_model_node_removed: + * @tree_model: + * @parent_node: + * @removed_node: + * + * + **/ +void +e_tree_model_node_removed (ETreeModel *tree_model, + ETreePath parent_node, + ETreePath removed_node, + gint old_position) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit ( + tree_model, signals[NODE_REMOVED], 0, + parent_node, removed_node, old_position); +} + +/** + * e_tree_model_node_deleted: + * @tree_model: + * @deleted_node: + * + * + **/ +void +e_tree_model_node_deleted (ETreeModel *tree_model, + ETreePath deleted_node) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit (tree_model, signals[NODE_DELETED], 0, deleted_node); +} + +/** + * e_tree_model_node_request_collapse: + * @tree_model: + * @collapsed_node: + * + * + **/ +void +e_tree_model_node_request_collapse (ETreeModel *tree_model, + ETreePath collapsed_node) +{ + g_return_if_fail (E_IS_TREE_MODEL (tree_model)); + + g_signal_emit (tree_model, signals[NODE_REQUEST_COLLAPSE], 0, collapsed_node); +} + +/** + * e_tree_model_new + * + * XXX docs here. + * + * return values: a newly constructed ETreeModel. + */ +ETreeModel * +e_tree_model_new (void) +{ + return g_object_new (E_TYPE_TREE_MODEL, NULL); +} + +/** + * e_tree_model_get_root + * @etree: the ETreeModel of which we want the root node. + * + * Accessor for the root node of @etree. + * + * return values: the ETreePath corresponding to the root node. + */ +ETreePath +e_tree_model_get_root (ETreeModel *etree) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->get_root) + return ETM_CLASS (etree)->get_root (etree); + else + return NULL; +} + +/** + * e_tree_model_node_get_parent: + * @etree: + * @path: + * + * + * + * Return value: + **/ +ETreePath +e_tree_model_node_get_parent (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->get_parent) + return ETM_CLASS (etree)->get_parent (etree, node); + else + return NULL; +} + +/** + * e_tree_model_node_get_first_child: + * @etree: + * @node: + * + * + * + * Return value: + **/ +ETreePath +e_tree_model_node_get_first_child (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->get_first_child) + return ETM_CLASS (etree)->get_first_child (etree, node); + else + return NULL; +} + +/** + * e_tree_model_node_get_last_child: + * @etree: + * @node: + * + * + * + * Return value: + **/ +ETreePath +e_tree_model_node_get_last_child (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->get_last_child) + return ETM_CLASS (etree)->get_last_child (etree, node); + else + return NULL; +} + +/** + * e_tree_model_node_get_next: + * @etree: + * @node: + * + * + * + * Return value: + **/ +ETreePath +e_tree_model_node_get_next (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->get_next) + return ETM_CLASS (etree)->get_next (etree, node); + else + return NULL; +} + +/** + * e_tree_model_node_get_prev: + * @etree: + * @node: + * + * + * + * Return value: + **/ +ETreePath +e_tree_model_node_get_prev (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->get_prev) + return ETM_CLASS (etree)->get_prev (etree, node); + else + return NULL; +} + +/** + * e_tree_model_node_is_root: + * @etree: + * @path: + * + * + * + * Return value: + **/ +gboolean +e_tree_model_node_is_root (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (etree != NULL, FALSE); + + if (ETM_CLASS (etree)->is_root) + return ETM_CLASS (etree)->is_root (etree, node); + else + return FALSE; +} + +/** + * e_tree_model_node_is_expandable: + * @etree: + * @path: + * + * + * + * Return value: + **/ +gboolean +e_tree_model_node_is_expandable (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (etree != NULL, FALSE); + g_return_val_if_fail (node != NULL, FALSE); + + if (ETM_CLASS (etree)->is_expandable) + return ETM_CLASS (etree)->is_expandable (etree, node); + else + return FALSE; +} + +guint +e_tree_model_node_get_children (ETreeModel *etree, + ETreePath node, + ETreePath **nodes) +{ + g_return_val_if_fail (etree != NULL, 0); + if (ETM_CLASS (etree)->get_children) + return ETM_CLASS (etree)->get_children (etree, node, nodes); + else + return 0; +} + +/** + * e_tree_model_node_depth: + * @etree: + * @path: + * + * + * + * Return value: + **/ +guint +e_tree_model_node_depth (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), 0); + + if (ETM_CLASS (etree)->depth) + return ETM_CLASS (etree)->depth (etree, node); + else + return 0; +} + +/** + * e_tree_model_icon_at + * @etree: The ETreeModel. + * @path: The ETreePath to the node we're getting the icon of. + * + * XXX docs here. + * + * return values: the GdkPixbuf associated with this node. + */ +GdkPixbuf * +e_tree_model_icon_at (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->icon_at) + return ETM_CLASS (etree)->icon_at (etree, node); + else + return NULL; +} + +/** + * e_tree_model_get_expanded_default + * @etree: The ETreeModel. + * + * XXX docs here. + * + * return values: Whether nodes should be expanded by default. + */ +gboolean +e_tree_model_get_expanded_default (ETreeModel *etree) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), FALSE); + + if (ETM_CLASS (etree)->get_expanded_default) + return ETM_CLASS (etree)->get_expanded_default (etree); + else + return FALSE; +} + +/** + * e_tree_model_column_count + * @etree: The ETreeModel. + * + * XXX docs here. + * + * return values: The number of columns + */ +gint +e_tree_model_column_count (ETreeModel *etree) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), 0); + + if (ETM_CLASS (etree)->column_count) + return ETM_CLASS (etree)->column_count (etree); + else + return 0; +} + +/** + * e_tree_model_has_save_id + * @etree: The ETreeModel. + * + * XXX docs here. + * + * return values: Whether this tree has valid save id data. + */ +gboolean +e_tree_model_has_save_id (ETreeModel *etree) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), FALSE); + + if (ETM_CLASS (etree)->has_save_id) + return ETM_CLASS (etree)->has_save_id (etree); + else + return FALSE; +} + +/** + * e_tree_model_get_save_id + * @etree: The ETreeModel. + * @node: The ETreePath. + * + * XXX docs here. + * + * return values: The save id for this path. + */ +gchar * +e_tree_model_get_save_id (ETreeModel *etree, + ETreePath node) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->get_save_id) + return ETM_CLASS (etree)->get_save_id (etree, node); + else + return NULL; +} + +/** + * e_tree_model_has_get_node_by_id + * @etree: The ETreeModel. + * + * XXX docs here. + * + * return values: Whether this tree can quickly get a node from its save id. + */ +gboolean +e_tree_model_has_get_node_by_id (ETreeModel *etree) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), FALSE); + + if (ETM_CLASS (etree)->has_get_node_by_id) + return ETM_CLASS (etree)->has_get_node_by_id (etree); + else + return FALSE; +} + +/** + * e_tree_model_get_node_by_id + * @etree: The ETreeModel. + * @node: The ETreePath. + * + * get_node_by_id(get_save_id(node)) should be the original node. + * Likewise if get_node_by_id is not NULL, then + * get_save_id(get_node_by_id(string)) should be a copy of the + * original string. + * + * return values: The path for this save id. + */ +ETreePath +e_tree_model_get_node_by_id (ETreeModel *etree, + const gchar *save_id) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->get_node_by_id) + return ETM_CLASS (etree)->get_node_by_id (etree, save_id); + else + return NULL; +} + +/** + * e_tree_model_has_change_pending + * @etree: The ETreeModel. + * + * XXX docs here. + * + * return values: Whether this tree has valid save id data. + */ +gboolean +e_tree_model_has_change_pending (ETreeModel *etree) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), FALSE); + + if (ETM_CLASS (etree)->has_change_pending) + return ETM_CLASS (etree)->has_change_pending (etree); + else + return FALSE; +} + +/** + * e_tree_model_sort_value_at: + * @etree: The ETreeModel. + * @node: The ETreePath to the node we're getting the data from. + * @col: the column to retrieve data from + * + * Return value: This function returns the value that is stored by the + * @etree in column @col and node @node. The data returned can be a + * pointer or any data value that can be stored inside a pointer. + * + * The data returned is typically used by an sort renderer if it wants + * to proxy the data of cell value_at at a better sorting order. + * + * The data returned must be valid until the model sends a signal that + * affect that piece of data. node_changed and node_deleted affect + * all data in tha t node and all nodes under that node. + * node_data_changed affects the data in that node. node_col_changed + * affects the data in that node for that column. node_inserted, + * node_removed, and no_change don't affect any data in this way. + **/ +gpointer +e_tree_model_sort_value_at (ETreeModel *etree, + ETreePath node, + gint col) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->sort_value_at) + return ETM_CLASS (etree)->sort_value_at (etree, node, col); + else + return NULL; +} + +/** + * e_tree_model_value_at: + * @etree: The ETreeModel. + * @node: The ETreePath to the node we're getting the data from. + * @col: the column to retrieve data from + * + * Return value: This function returns the value that is stored by the + * @etree in column @col and node @node. The data returned can be a + * pointer or any data value that can be stored inside a pointer. + * + * The data returned is typically used by an ECell renderer. + * + * The data returned must be valid until the model sends a signal that + * affect that piece of data. node_changed and node_deleted affect + * all data in tha t node and all nodes under that node. + * node_data_changed affects the data in that node. node_col_changed + * affects the data in that node for that column. node_inserted, + * node_removed, and no_change don't affect any data in this way. + **/ +gpointer +e_tree_model_value_at (ETreeModel *etree, + ETreePath node, + gint col) +{ + g_return_val_if_fail (E_IS_TREE_MODEL (etree), NULL); + + if (ETM_CLASS (etree)->value_at) + return ETM_CLASS (etree)->value_at (etree, node, col); + else + return NULL; +} + +void +e_tree_model_set_value_at (ETreeModel *etree, + ETreePath node, + gint col, + gconstpointer val) +{ + g_return_if_fail (E_IS_TREE_MODEL (etree)); + + if (ETM_CLASS (etree)->set_value_at) + ETM_CLASS (etree)->set_value_at (etree, node, col, val); +} + +/** + * e_tree_model_node_is_editable: + * @etree: + * @path: + * + * + * + * Return value: + **/ +gboolean +e_tree_model_node_is_editable (ETreeModel *etree, + ETreePath node, + gint col) +{ + g_return_val_if_fail (etree != NULL, FALSE); + + if (ETM_CLASS (etree)->is_editable) + return ETM_CLASS (etree)->is_editable (etree, node, col); + else + return FALSE; +} + +/** + * e_tree_model_duplicate_value: + * @etree: + * @path: + * + * + * + * Return value: + **/ +gpointer +e_tree_model_duplicate_value (ETreeModel *etree, + gint col, + gconstpointer value) +{ + g_return_val_if_fail (etree != NULL, NULL); + + if (ETM_CLASS (etree)->duplicate_value) + return ETM_CLASS (etree)->duplicate_value (etree, col, value); + else + return NULL; +} + +/** + * e_tree_model_free_value: + * @etree: + * @path: + * + * + * + * Return value: + **/ +void +e_tree_model_free_value (ETreeModel *etree, + gint col, + gpointer value) +{ + g_return_if_fail (etree != NULL); + + if (ETM_CLASS (etree)->free_value) + ETM_CLASS (etree)->free_value (etree, col, value); +} + +/** + * e_tree_model_initialize_value: + * @etree: + * @path: + * + * + * + * Return value: + **/ +gpointer +e_tree_model_initialize_value (ETreeModel *etree, + gint col) +{ + g_return_val_if_fail (etree != NULL, NULL); + + if (ETM_CLASS (etree)->initialize_value) + return ETM_CLASS (etree)->initialize_value (etree, col); + else + return NULL; +} + +/** + * e_tree_model_value_is_empty: + * @etree: + * @path: + * + * + * + * Return value: + **/ +gboolean +e_tree_model_value_is_empty (ETreeModel *etree, + gint col, + gconstpointer value) +{ + g_return_val_if_fail (etree != NULL, TRUE); + + if (ETM_CLASS (etree)->value_is_empty) + return ETM_CLASS (etree)->value_is_empty (etree, col, value); + else + return TRUE; +} + +/** + * e_tree_model_value_to_string: + * @etree: + * @path: + * + * + * + * Return value: + **/ +gchar * +e_tree_model_value_to_string (ETreeModel *etree, + gint col, + gconstpointer value) +{ + g_return_val_if_fail (etree != NULL, g_strdup ("")); + + if (ETM_CLASS (etree)->value_to_string) + return ETM_CLASS (etree)->value_to_string (etree, col, value); + else + return g_strdup (""); +} + +/** + * e_tree_model_node_traverse: + * @model: + * @path: + * @func: + * @data: + * + * + **/ +void +e_tree_model_node_traverse (ETreeModel *model, + ETreePath path, + ETreePathFunc func, + gpointer data) +{ + ETreePath child; + + g_return_if_fail (E_IS_TREE_MODEL (model)); + g_return_if_fail (path != NULL); + + child = e_tree_model_node_get_first_child (model, path); + + while (child) { + ETreePath next_child; + + next_child = e_tree_model_node_get_next (model, child); + e_tree_model_node_traverse (model, child, func, data); + if (func (model, child, data)) + return; + + child = next_child; + } +} + +/** + * e_tree_model_node_traverse_preorder: + * @model: + * @path: + * @func: + * @data: + * + * + **/ +void +e_tree_model_node_traverse_preorder (ETreeModel *model, + ETreePath path, + ETreePathFunc func, + gpointer data) +{ + ETreePath child; + + g_return_if_fail (E_IS_TREE_MODEL (model)); + g_return_if_fail (path != NULL); + + child = e_tree_model_node_get_first_child (model, path); + + while (child) { + ETreePath next_child; + + if (func (model, child, data)) + return; + + next_child = e_tree_model_node_get_next (model, child); + e_tree_model_node_traverse_preorder (model, child, func, data); + + child = next_child; + } +} + +/** + * e_tree_model_node_traverse_preorder: + * @model: + * @path: + * @func: + * @data: + * + * + **/ +static ETreePath +e_tree_model_node_real_traverse (ETreeModel *model, + ETreePath path, + ETreePath end_path, + gboolean forward_direction, + ETreePathFunc func, + gpointer data) +{ + ETreePath child; + + g_return_val_if_fail (E_IS_TREE_MODEL (model), NULL); + g_return_val_if_fail (path != NULL, NULL); + + if (forward_direction) + child = e_tree_model_node_get_first_child (model, path); + else + child = e_tree_model_node_get_last_child (model, path); + + while (child) { + ETreePath result; + + if (forward_direction && (child == end_path || func (model, child, data))) + return child; + + if ((result = e_tree_model_node_real_traverse ( + model, child, end_path, + forward_direction, func, data))) + return result; + + if (!forward_direction && (child == end_path || func (model, child, data))) + return child; + + if (forward_direction) + child = e_tree_model_node_get_next (model, child); + else + child = e_tree_model_node_get_prev (model, child); + } + return NULL; +} + +/** + * e_tree_model_node_traverse_preorder: + * @model: + * @path: + * @func: + * @data: + * + * + **/ +ETreePath +e_tree_model_node_find (ETreeModel *model, + ETreePath path, + ETreePath end_path, + gboolean forward_direction, + ETreePathFunc func, + gpointer data) +{ + ETreePath result; + ETreePath next; + + g_return_val_if_fail (E_IS_TREE_MODEL (model), NULL); + + /* Just search the whole tree in this case. */ + if (path == NULL) { + ETreePath root; + root = e_tree_model_get_root (model); + + if (forward_direction && (end_path == root || func (model, root, data))) + return root; + + result = e_tree_model_node_real_traverse ( + model, root, end_path, forward_direction, func, data); + if (result) + return result; + + if (!forward_direction && (end_path == root || func (model, root, data))) + return root; + + return NULL; + } + + while (1) { + + if (forward_direction) { + if ((result = e_tree_model_node_real_traverse ( + model, path, end_path, + forward_direction, func, data))) + return result; + next = e_tree_model_node_get_next (model, path); + } else { + next = e_tree_model_node_get_prev (model, path); + if (next && (result = e_tree_model_node_real_traverse ( + model, next, end_path, + forward_direction, func, data))) + return result; + } + + while (next == NULL) { + path = e_tree_model_node_get_parent (model, path); + + if (path == NULL) + return NULL; + + if (forward_direction) + next = e_tree_model_node_get_next (model, path); + else + next = path; + } + + if (end_path == next || func (model, next, data)) + return next; + + path = next; + } +} + diff --git a/e-util/e-tree-model.h b/e-util/e-tree-model.h new file mode 100644 index 0000000000..1d02615a45 --- /dev/null +++ b/e-util/e-tree-model.h @@ -0,0 +1,298 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Chris Toshok <toshok@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TREE_MODEL_H_ +#define _E_TREE_MODEL_H_ + +#include <gdk-pixbuf/gdk-pixbuf.h> + +/* Standard GObject macros */ +#define E_TYPE_TREE_MODEL \ + (e_tree_model_get_type ()) +#define E_TREE_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TREE_MODEL, ETreeModel)) +#define E_TREE_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TREE_MODEL, ETreeModelClass)) +#define E_IS_TREE_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TREE_MODEL)) +#define E_IS_TREE_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TREE_MODEL)) +#define E_TREE_MODEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TREE_MODEL, ETreeModelClass)) + +G_BEGIN_DECLS + +typedef gpointer ETreePath; + +typedef struct _ETreeModel ETreeModel; +typedef struct _ETreeModelClass ETreeModelClass; + +typedef gint (*ETreePathCompareFunc) (ETreeModel *model, + ETreePath path1, + ETreePath path2); +typedef gboolean (*ETreePathFunc) (ETreeModel *model, + ETreePath path, + gpointer data); + +struct _ETreeModel { + GObject parent; +}; + +struct _ETreeModelClass { + GObjectClass parent_class; + + /* + * Virtual methods + */ + ETreePath (*get_root) (ETreeModel *etm); + + ETreePath (*get_parent) (ETreeModel *etm, + ETreePath node); + ETreePath (*get_first_child) (ETreeModel *etm, + ETreePath node); + ETreePath (*get_last_child) (ETreeModel *etm, + ETreePath node); + ETreePath (*get_next) (ETreeModel *etm, + ETreePath node); + ETreePath (*get_prev) (ETreeModel *etm, + ETreePath node); + + gboolean (*is_root) (ETreeModel *etm, + ETreePath node); + gboolean (*is_expandable) (ETreeModel *etm, + ETreePath node); + guint (*get_children) (ETreeModel *etm, + ETreePath node, + ETreePath **paths); + guint (*depth) (ETreeModel *etm, + ETreePath node); + + GdkPixbuf * (*icon_at) (ETreeModel *etm, + ETreePath node); + + gboolean (*get_expanded_default) (ETreeModel *etm); + gint (*column_count) (ETreeModel *etm); + + gboolean (*has_save_id) (ETreeModel *etm); + gchar * (*get_save_id) (ETreeModel *etm, + ETreePath node); + + gboolean (*has_get_node_by_id) (ETreeModel *etm); + ETreePath (*get_node_by_id) (ETreeModel *etm, + const gchar *save_id); + + gboolean (*has_change_pending) (ETreeModel *etm); + + /* + * ETable analogs + */ + gpointer (*sort_value_at) (ETreeModel *etm, + ETreePath node, + gint col); + gpointer (*value_at) (ETreeModel *etm, + ETreePath node, + gint col); + void (*set_value_at) (ETreeModel *etm, + ETreePath node, + gint col, + gconstpointer val); + gboolean (*is_editable) (ETreeModel *etm, + ETreePath node, + gint col); + + gpointer (*duplicate_value) (ETreeModel *etm, + gint col, + gconstpointer value); + void (*free_value) (ETreeModel *etm, + gint col, + gpointer value); + gpointer (*initialize_value) (ETreeModel *etm, + gint col); + gboolean (*value_is_empty) (ETreeModel *etm, + gint col, + gconstpointer value); + gchar * (*value_to_string) (ETreeModel *etm, + gint col, + gconstpointer value); + + /* + * Signals + */ + + /* During node_remove, the ETreePath of the child is removed + * from the tree but is still a valid ETreePath. At + * node_deleted, the ETreePath is no longer valid. + */ + + void (*pre_change) (ETreeModel *etm); + void (*no_change) (ETreeModel *etm); + void (*node_changed) (ETreeModel *etm, + ETreePath node); + void (*node_data_changed) (ETreeModel *etm, + ETreePath node); + void (*node_col_changed) (ETreeModel *etm, + ETreePath node, + gint col); + void (*node_inserted) (ETreeModel *etm, + ETreePath parent, + ETreePath inserted_node); + void (*node_removed) (ETreeModel *etm, + ETreePath parent, + ETreePath removed_node, + gint old_position); + void (*node_deleted) (ETreeModel *etm, + ETreePath deleted_node); + void (*rebuilt) (ETreeModel *etm); + + /* This signal requests that any viewers of the tree that + * collapse and expand nodes collapse this node. + */ + void (*node_request_collapse) + (ETreeModel *etm, + ETreePath node); +}; + +GType e_tree_model_get_type (void) G_GNUC_CONST; +ETreeModel * e_tree_model_new (void); + +/* tree traversal operations */ +ETreePath e_tree_model_get_root (ETreeModel *etree); +ETreePath e_tree_model_node_get_parent (ETreeModel *etree, + ETreePath path); +ETreePath e_tree_model_node_get_first_child + (ETreeModel *etree, + ETreePath path); +ETreePath e_tree_model_node_get_last_child + (ETreeModel *etree, + ETreePath path); +ETreePath e_tree_model_node_get_next (ETreeModel *etree, + ETreePath path); +ETreePath e_tree_model_node_get_prev (ETreeModel *etree, + ETreePath path); + +/* node accessors */ +gboolean e_tree_model_node_is_root (ETreeModel *etree, + ETreePath path); +gboolean e_tree_model_node_is_expandable (ETreeModel *etree, + ETreePath path); +guint e_tree_model_node_get_children (ETreeModel *etree, + ETreePath path, + ETreePath **paths); +guint e_tree_model_node_depth (ETreeModel *etree, + ETreePath path); +GdkPixbuf * e_tree_model_icon_at (ETreeModel *etree, + ETreePath path); +gboolean e_tree_model_get_expanded_default + (ETreeModel *model); +gint e_tree_model_column_count (ETreeModel *model); +gboolean e_tree_model_has_save_id (ETreeModel *model); +gchar * e_tree_model_get_save_id (ETreeModel *model, + ETreePath node); +gboolean e_tree_model_has_get_node_by_id (ETreeModel *model); +ETreePath e_tree_model_get_node_by_id (ETreeModel *model, + const gchar *save_id); +gboolean e_tree_model_has_change_pending (ETreeModel *model); +void *e_tree_model_sort_value_at (ETreeModel *etree, + ETreePath node, + gint col); +void *e_tree_model_value_at (ETreeModel *etree, + ETreePath node, + gint col); +void e_tree_model_set_value_at (ETreeModel *etree, + ETreePath node, + gint col, + gconstpointer val); +gboolean e_tree_model_node_is_editable (ETreeModel *etree, + ETreePath node, + gint col); +void *e_tree_model_duplicate_value (ETreeModel *etree, + gint col, + gconstpointer value); +void e_tree_model_free_value (ETreeModel *etree, + gint col, + gpointer value); +void *e_tree_model_initialize_value (ETreeModel *etree, + gint col); +gboolean e_tree_model_value_is_empty (ETreeModel *etree, + gint col, + gconstpointer value); +gchar * e_tree_model_value_to_string (ETreeModel *etree, + gint col, + gconstpointer value); + +/* depth first traversal of path's descendents, calling func on each one */ +void e_tree_model_node_traverse (ETreeModel *model, + ETreePath path, + ETreePathFunc func, + gpointer data); +void e_tree_model_node_traverse_preorder + (ETreeModel *model, + ETreePath path, + ETreePathFunc func, + gpointer data); +ETreePath e_tree_model_node_find (ETreeModel *model, + ETreePath path, + ETreePath end_path, + gboolean forward_direction, + ETreePathFunc func, + gpointer data); + +/* +** Routines for emitting signals on the ETreeModel +*/ +void e_tree_model_pre_change (ETreeModel *tree_model); +void e_tree_model_no_change (ETreeModel *tree_model); +void e_tree_model_rebuilt (ETreeModel *tree_model); +void e_tree_model_node_changed (ETreeModel *tree_model, + ETreePath node); +void e_tree_model_node_data_changed (ETreeModel *tree_model, + ETreePath node); +void e_tree_model_node_col_changed (ETreeModel *tree_model, + ETreePath node, + gint col); +void e_tree_model_node_inserted (ETreeModel *tree_model, + ETreePath parent_node, + ETreePath inserted_node); +void e_tree_model_node_removed (ETreeModel *tree_model, + ETreePath parent_node, + ETreePath removed_node, + gint old_position); +void e_tree_model_node_deleted (ETreeModel *tree_model, + ETreePath deleted_node); +void e_tree_model_node_request_collapse + (ETreeModel *tree_model, + ETreePath deleted_node); + +G_END_DECLS + +#endif /* _E_TREE_MODEL_H */ diff --git a/e-util/e-tree-selection-model.c b/e-util/e-tree-selection-model.c new file mode 100644 index 0000000000..480b5a4e8a --- /dev/null +++ b/e-util/e-tree-selection-model.c @@ -0,0 +1,939 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Mike Kestner <mkestner@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-tree-selection-model.h" + +#include <glib/gi18n.h> + +#include "e-tree-table-adapter.h" + +#define E_TREE_SELECTION_MODEL_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TREE_SELECTION_MODEL, ETreeSelectionModelPrivate)) + +G_DEFINE_TYPE ( + ETreeSelectionModel, e_tree_selection_model, E_TYPE_SELECTION_MODEL) + +enum { + PROP_0, + PROP_CURSOR_ROW, + PROP_CURSOR_COL, + PROP_MODEL, + PROP_ETTA +}; + +struct _ETreeSelectionModelPrivate { + ETreeTableAdapter *etta; + ETreeModel *model; + + GHashTable *paths; + ETreePath cursor_path; + ETreePath start_path; + gint cursor_col; + gchar *cursor_save_id; + + gint tree_model_pre_change_id; + gint tree_model_no_change_id; + gint tree_model_node_changed_id; + gint tree_model_node_data_changed_id; + gint tree_model_node_col_changed_id; + gint tree_model_node_inserted_id; + gint tree_model_node_removed_id; + gint tree_model_node_deleted_id; +}; + +static gint +get_cursor_row (ETreeSelectionModel *etsm) +{ + if (etsm->priv->cursor_path) + return e_tree_table_adapter_row_of_node ( + etsm->priv->etta, etsm->priv->cursor_path); + + return -1; +} + +static void +clear_selection (ETreeSelectionModel *etsm) +{ + g_hash_table_destroy (etsm->priv->paths); + etsm->priv->paths = g_hash_table_new (NULL, NULL); +} + +static void +change_one_path (ETreeSelectionModel *etsm, + ETreePath path, + gboolean grow) +{ + if (!path) + return; + + if (grow) + g_hash_table_insert (etsm->priv->paths, path, path); + else if (g_hash_table_lookup (etsm->priv->paths, path)) + g_hash_table_remove (etsm->priv->paths, path); +} + +static void +select_single_path (ETreeSelectionModel *etsm, + ETreePath path) +{ + clear_selection (etsm); + change_one_path (etsm, path, TRUE); + etsm->priv->cursor_path = path; + etsm->priv->start_path = NULL; +} + +static void +select_range (ETreeSelectionModel *etsm, + gint start, + gint end) +{ + gint i; + + if (start > end) { + i = start; + start = end; + end = i; + } + + for (i = start; i <= end; i++) { + ETreePath path = e_tree_table_adapter_node_at_row (etsm->priv->etta, i); + if (path) + g_hash_table_insert (etsm->priv->paths, path, path); + } +} + +static void +free_id (ETreeSelectionModel *etsm) +{ + g_free (etsm->priv->cursor_save_id); + etsm->priv->cursor_save_id = NULL; +} + +static void +restore_cursor (ETreeSelectionModel *etsm, + ETreeModel *etm) +{ + clear_selection (etsm); + etsm->priv->cursor_path = NULL; + + if (etsm->priv->cursor_save_id) { + etsm->priv->cursor_path = e_tree_model_get_node_by_id ( + etm, etsm->priv->cursor_save_id); + if (etsm->priv->cursor_path != NULL && etsm->priv->cursor_col == -1) + etsm->priv->cursor_col = 0; + + select_single_path (etsm, etsm->priv->cursor_path); + } + + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); + + if (etsm->priv->cursor_path) { + gint cursor_row = get_cursor_row (etsm); + e_selection_model_cursor_changed ( + E_SELECTION_MODEL (etsm), + cursor_row, etsm->priv->cursor_col); + } else { + e_selection_model_cursor_changed ( + E_SELECTION_MODEL (etsm), -1, -1); + e_selection_model_cursor_activated ( + E_SELECTION_MODEL (etsm), -1, -1); + + } + + free_id (etsm); +} + +static void +etsm_pre_change (ETreeModel *etm, + ETreeSelectionModel *etsm) +{ + g_free (etsm->priv->cursor_save_id); + etsm->priv->cursor_save_id = NULL; + + if (e_tree_model_has_get_node_by_id (etm) && + e_tree_model_has_save_id (etm) && + etsm->priv->cursor_path) { + etsm->priv->cursor_save_id = e_tree_model_get_save_id ( + etm, etsm->priv->cursor_path); + } +} + +static void +etsm_no_change (ETreeModel *etm, + ETreeSelectionModel *etsm) +{ + free_id (etsm); +} + +static void +etsm_node_changed (ETreeModel *etm, + ETreePath node, + ETreeSelectionModel *etsm) +{ + restore_cursor (etsm, etm); +} + +static void +etsm_node_data_changed (ETreeModel *etm, + ETreePath node, + ETreeSelectionModel *etsm) +{ + free_id (etsm); +} + +static void +etsm_node_col_changed (ETreeModel *etm, + ETreePath node, + gint col, + ETreeSelectionModel *etsm) +{ + free_id (etsm); +} + +static void +etsm_node_inserted (ETreeModel *etm, + ETreePath parent, + ETreePath child, + ETreeSelectionModel *etsm) +{ + restore_cursor (etsm, etm); +} + +static void +etsm_node_removed (ETreeModel *etm, + ETreePath parent, + ETreePath child, + gint old_position, + ETreeSelectionModel *etsm) +{ + restore_cursor (etsm, etm); +} + +static void +etsm_node_deleted (ETreeModel *etm, + ETreePath child, + ETreeSelectionModel *etsm) +{ + restore_cursor (etsm, etm); +} + +static void +add_model (ETreeSelectionModel *etsm, + ETreeModel *model) +{ + ETreeSelectionModelPrivate *priv = etsm->priv; + + priv->model = model; + + if (!priv->model) + return; + + g_object_ref (priv->model); + + priv->tree_model_pre_change_id = g_signal_connect_after ( + priv->model, "pre_change", + G_CALLBACK (etsm_pre_change), etsm); + + priv->tree_model_no_change_id = g_signal_connect_after ( + priv->model, "no_change", + G_CALLBACK (etsm_no_change), etsm); + + priv->tree_model_node_changed_id = g_signal_connect_after ( + priv->model, "node_changed", + G_CALLBACK (etsm_node_changed), etsm); + + priv->tree_model_node_data_changed_id = g_signal_connect_after ( + priv->model, "node_data_changed", + G_CALLBACK (etsm_node_data_changed), etsm); + + priv->tree_model_node_col_changed_id = g_signal_connect_after ( + priv->model, "node_col_changed", + G_CALLBACK (etsm_node_col_changed), etsm); + + priv->tree_model_node_inserted_id = g_signal_connect_after ( + priv->model, "node_inserted", + G_CALLBACK (etsm_node_inserted), etsm); + + priv->tree_model_node_removed_id = g_signal_connect_after ( + priv->model, "node_removed", + G_CALLBACK (etsm_node_removed), etsm); + + priv->tree_model_node_deleted_id = g_signal_connect_after ( + priv->model, "node_deleted", + G_CALLBACK (etsm_node_deleted), etsm); +} + +static void +drop_model (ETreeSelectionModel *etsm) +{ + ETreeSelectionModelPrivate *priv = etsm->priv; + + if (!priv->model) + return; + + g_signal_handler_disconnect ( + priv->model, priv->tree_model_pre_change_id); + g_signal_handler_disconnect ( + priv->model, priv->tree_model_no_change_id); + g_signal_handler_disconnect ( + priv->model, priv->tree_model_node_changed_id); + g_signal_handler_disconnect ( + priv->model, priv->tree_model_node_data_changed_id); + g_signal_handler_disconnect ( + priv->model, priv->tree_model_node_col_changed_id); + g_signal_handler_disconnect ( + priv->model, priv->tree_model_node_inserted_id); + g_signal_handler_disconnect ( + priv->model, priv->tree_model_node_removed_id); + g_signal_handler_disconnect ( + priv->model, priv->tree_model_node_deleted_id); + + g_object_unref (priv->model); + priv->model = NULL; + + priv->tree_model_pre_change_id = 0; + priv->tree_model_no_change_id = 0; + priv->tree_model_node_changed_id = 0; + priv->tree_model_node_data_changed_id = 0; + priv->tree_model_node_col_changed_id = 0; + priv->tree_model_node_inserted_id = 0; + priv->tree_model_node_removed_id = 0; + priv->tree_model_node_deleted_id = 0; +} + +static void +etsm_dispose (GObject *object) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (object); + + drop_model (etsm); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_tree_selection_model_parent_class)->dispose (object); +} + +static void +etsm_finalize (GObject *object) +{ + ETreeSelectionModelPrivate *priv; + + priv = E_TREE_SELECTION_MODEL_GET_PRIVATE (object); + + clear_selection (E_TREE_SELECTION_MODEL (object)); + g_hash_table_destroy (priv->paths); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_tree_selection_model_parent_class)->finalize (object); +} + +static void +etsm_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (object); + + switch (property_id) { + case PROP_CURSOR_ROW: + g_value_set_int (value, get_cursor_row (etsm)); + break; + + case PROP_CURSOR_COL: + g_value_set_int (value, etsm->priv->cursor_col); + break; + + case PROP_MODEL: + g_value_set_object (value, etsm->priv->model); + break; + + case PROP_ETTA: + g_value_set_object (value, etsm->priv->etta); + break; + } +} + +static void +etsm_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ESelectionModel *esm = E_SELECTION_MODEL (object); + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (object); + + switch (property_id) { + case PROP_CURSOR_ROW: + e_selection_model_do_something ( + esm, g_value_get_int (value), + etsm->priv->cursor_col, 0); + break; + + case PROP_CURSOR_COL: + e_selection_model_do_something ( + esm, get_cursor_row (etsm), + g_value_get_int (value), 0); + break; + + case PROP_MODEL: + drop_model (etsm); + add_model (etsm, E_TREE_MODEL (g_value_get_object (value))); + break; + + case PROP_ETTA: + etsm->priv->etta = + E_TREE_TABLE_ADAPTER (g_value_get_object (value)); + break; + } +} + +static gboolean +etsm_is_path_selected (ETreeSelectionModel *etsm, + ETreePath path) +{ + if (path && g_hash_table_lookup (etsm->priv->paths, path)) + return TRUE; + + return FALSE; +} + +/** + * e_selection_model_is_row_selected + * @selection: #ESelectionModel to check + * @n: The row to check + * + * This routine calculates whether the given row is selected. + * + * Returns: %TRUE if the given row is selected + */ +static gboolean +etsm_is_row_selected (ESelectionModel *selection, + gint row) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + ETreePath path; + + g_return_val_if_fail ( + row < e_table_model_row_count ( + E_TABLE_MODEL (etsm->priv->etta)), FALSE); + g_return_val_if_fail (row >= 0, FALSE); + g_return_val_if_fail (etsm != NULL, FALSE); + + path = e_tree_table_adapter_node_at_row (etsm->priv->etta, row); + return etsm_is_path_selected (etsm, path); +} + +typedef struct { + ETreeSelectionModel *etsm; + EForeachFunc callback; + gpointer closure; +} ModelAndCallback; + +static void +etsm_row_foreach_cb (gpointer key, + gpointer value, + gpointer user_data) +{ + ETreePath path = key; + ModelAndCallback *mac = user_data; + gint row = e_tree_table_adapter_row_of_node ( + mac->etsm->priv->etta, path); + if (row >= 0) + mac->callback (row, mac->closure); +} + +/** + * e_selection_model_foreach + * @selection: #ESelectionModel to traverse + * @callback: The callback function to call back. + * @closure: The closure + * + * This routine calls the given callback function once for each + * selected row, passing closure as the closure. + */ +static void +etsm_foreach (ESelectionModel *selection, + EForeachFunc callback, + gpointer closure) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + ModelAndCallback mac; + + mac.etsm = etsm; + mac.callback = callback; + mac.closure = closure; + + g_hash_table_foreach (etsm->priv->paths, etsm_row_foreach_cb, &mac); +} + +/** + * e_selection_model_clear + * @selection: #ESelectionModel to clear + * + * This routine clears the selection to no rows selected. + */ +static void +etsm_clear (ESelectionModel *selection) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + + clear_selection (etsm); + + etsm->priv->cursor_path = NULL; + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); + e_selection_model_cursor_changed (E_SELECTION_MODEL (etsm), -1, -1); +} + +/** + * e_selection_model_selected_count + * @selection: #ESelectionModel to count + * + * This routine calculates the number of rows selected. + * + * Returns: The number of rows selected in the given model. + */ +static gint +etsm_selected_count (ESelectionModel *selection) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + + return g_hash_table_size (etsm->priv->paths); +} + +static gint +etsm_row_count (ESelectionModel *selection) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + return e_table_model_row_count (E_TABLE_MODEL (etsm->priv->etta)); +} + +/** + * e_selection_model_select_all + * @selection: #ESelectionModel to select all + * + * This routine selects all the rows in the given + * #ESelectionModel. + */ +static void +etsm_select_all (ESelectionModel *selection) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + ETreePath root; + + root = e_tree_model_get_root (etsm->priv->model); + if (root == NULL) + return; + + clear_selection (etsm); + select_range (etsm, 0, etsm_row_count (selection) - 1); + + if (etsm->priv->cursor_path == NULL) + etsm->priv->cursor_path = e_tree_table_adapter_node_at_row ( + etsm->priv->etta, 0); + + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); + + e_selection_model_cursor_changed ( + E_SELECTION_MODEL (etsm), + get_cursor_row (etsm), etsm->priv->cursor_col); +} + +/** + * e_selection_model_invert_selection + * @selection: #ESelectionModel to invert + * + * This routine inverts all the rows in the given + * #ESelectionModel. + */ +static void +etsm_invert_selection (ESelectionModel *selection) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + gint count = etsm_row_count (selection); + gint i; + + for (i = 0; i < count; i++) { + ETreePath path; + + path = e_tree_table_adapter_node_at_row (etsm->priv->etta, i); + if (!path) + continue; + if (g_hash_table_lookup (etsm->priv->paths, path)) + g_hash_table_remove (etsm->priv->paths, path); + else + g_hash_table_insert (etsm->priv->paths, path, path); + } + + etsm->priv->cursor_col = -1; + etsm->priv->cursor_path = NULL; + etsm->priv->start_path = NULL; + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); + e_selection_model_cursor_changed (E_SELECTION_MODEL (etsm), -1, -1); +} + +static void +etsm_change_one_row (ESelectionModel *selection, + gint row, + gboolean grow) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + ETreePath path; + + g_return_if_fail ( + row < e_table_model_row_count ( + E_TABLE_MODEL (etsm->priv->etta))); + g_return_if_fail (row >= 0); + g_return_if_fail (selection != NULL); + + path = e_tree_table_adapter_node_at_row (etsm->priv->etta, row); + + if (!path) + return; + + change_one_path (etsm, path, grow); +} + +static void +etsm_change_cursor (ESelectionModel *selection, + gint row, + gint col) +{ + ETreeSelectionModel *etsm; + + g_return_if_fail (selection != NULL); + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + etsm = E_TREE_SELECTION_MODEL (selection); + + if (row == -1) { + etsm->priv->cursor_path = NULL; + } else { + etsm->priv->cursor_path = + e_tree_table_adapter_node_at_row ( + etsm->priv->etta, row); + } + etsm->priv->cursor_col = col; +} + +static gint +etsm_cursor_row (ESelectionModel *selection) +{ + return get_cursor_row (E_TREE_SELECTION_MODEL (selection)); +} + +static gint +etsm_cursor_col (ESelectionModel *selection) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + return etsm->priv->cursor_col; +} + +static void +etsm_get_rows (gint row, + gpointer d) +{ + gint **rowp = d; + + **rowp = row; + (*rowp)++; +} + +static void +etsm_select_single_row (ESelectionModel *selection, + gint row) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + ETreePath path; + gint rows[5], *rowp = NULL, size; + + path = e_tree_table_adapter_node_at_row (etsm->priv->etta, row); + g_return_if_fail (path != NULL); + + /* we really only care about the size=1 case (cursor changed), + * but this doesn't cost much */ + size = g_hash_table_size (etsm->priv->paths); + if (size > 0 && size <= 5) { + rowp = rows; + etsm_foreach (selection, etsm_get_rows, &rowp); + } + + select_single_path (etsm, path); + + if (size > 5) { + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); + } else { + if (rowp) { + gint *p = rows; + + while (p < rowp) + e_selection_model_selection_row_changed ( + (ESelectionModel *) etsm, *p++); + } + e_selection_model_selection_row_changed ( + (ESelectionModel *) etsm, row); + } +} + +static void +etsm_toggle_single_row (ESelectionModel *selection, + gint row) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + ETreePath path; + + path = e_tree_table_adapter_node_at_row (etsm->priv->etta, row); + g_return_if_fail (path); + + if (g_hash_table_lookup (etsm->priv->paths, path)) + g_hash_table_remove (etsm->priv->paths, path); + else + g_hash_table_insert (etsm->priv->paths, path, path); + + etsm->priv->start_path = NULL; + + e_selection_model_selection_row_changed ((ESelectionModel *) etsm, row); +} + +static void +etsm_real_move_selection_end (ETreeSelectionModel *etsm, + gint row) +{ + ETreePath end_path; + gint start; + + end_path = e_tree_table_adapter_node_at_row (etsm->priv->etta, row); + g_return_if_fail (end_path); + + start = e_tree_table_adapter_row_of_node ( + etsm->priv->etta, etsm->priv->start_path); + clear_selection (etsm); + select_range (etsm, start, row); +} + +static void +etsm_move_selection_end (ESelectionModel *selection, + gint row) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + + g_return_if_fail (etsm->priv->cursor_path); + + etsm_real_move_selection_end (etsm, row); + e_selection_model_selection_changed (E_SELECTION_MODEL (selection)); +} + +static void +etsm_set_selection_end (ESelectionModel *selection, + gint row) +{ + ETreeSelectionModel *etsm = E_TREE_SELECTION_MODEL (selection); + + g_return_if_fail (etsm->priv->cursor_path); + + if (!etsm->priv->start_path) + etsm->priv->start_path = etsm->priv->cursor_path; + etsm_real_move_selection_end (etsm, row); + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); +} + +struct foreach_path_t { + ETreeForeachFunc callback; + gpointer closure; +}; + +static void +foreach_path (gpointer key, + gpointer value, + gpointer data) +{ + ETreePath path = key; + struct foreach_path_t *c = data; + c->callback (path, c->closure); +} + +void +e_tree_selection_model_foreach (ETreeSelectionModel *etsm, + ETreeForeachFunc callback, + gpointer closure) +{ + if (etsm->priv->paths) { + struct foreach_path_t c; + c.callback = callback; + c.closure = closure; + g_hash_table_foreach (etsm->priv->paths, foreach_path, &c); + return; + } +} + +void +e_tree_selection_model_select_single_path (ETreeSelectionModel *etsm, + ETreePath path) +{ + select_single_path (etsm, path); + + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); +} + +void +e_tree_selection_model_select_paths (ETreeSelectionModel *etsm, + GPtrArray *paths) +{ + ETreePath path; + gint i; + + for (i = 0; i < paths->len; i++) { + path = paths->pdata[i]; + change_one_path (etsm, path, TRUE); + } + + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); +} + +void +e_tree_selection_model_add_to_selection (ETreeSelectionModel *etsm, + ETreePath path) +{ + change_one_path (etsm, path, TRUE); + + e_selection_model_selection_changed (E_SELECTION_MODEL (etsm)); +} + +void +e_tree_selection_model_change_cursor (ETreeSelectionModel *etsm, + ETreePath path) +{ + gint row; + + etsm->priv->cursor_path = path; + + row = get_cursor_row (etsm); + + E_SELECTION_MODEL (etsm)->old_selection = -1; + + e_selection_model_cursor_changed ( + E_SELECTION_MODEL (etsm), row, etsm->priv->cursor_col); + e_selection_model_cursor_activated ( + E_SELECTION_MODEL (etsm), row, etsm->priv->cursor_col); +} + +ETreePath +e_tree_selection_model_get_cursor (ETreeSelectionModel *etsm) +{ + return etsm->priv->cursor_path; +} + +static void +e_tree_selection_model_init (ETreeSelectionModel *etsm) +{ + etsm->priv = E_TREE_SELECTION_MODEL_GET_PRIVATE (etsm); + + etsm->priv->paths = g_hash_table_new (NULL, NULL); + etsm->priv->cursor_col = -1; +} + +static void +e_tree_selection_model_class_init (ETreeSelectionModelClass *class) +{ + GObjectClass *object_class; + ESelectionModelClass *esm_class; + + g_type_class_add_private (class, sizeof (ETreeSelectionModelPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = etsm_dispose; + object_class->finalize = etsm_finalize; + object_class->get_property = etsm_get_property; + object_class->set_property = etsm_set_property; + + esm_class = E_SELECTION_MODEL_CLASS (class); + esm_class->is_row_selected = etsm_is_row_selected; + esm_class->foreach = etsm_foreach; + esm_class->clear = etsm_clear; + esm_class->selected_count = etsm_selected_count; + esm_class->select_all = etsm_select_all; + esm_class->invert_selection = etsm_invert_selection; + esm_class->row_count = etsm_row_count; + + esm_class->change_one_row = etsm_change_one_row; + esm_class->change_cursor = etsm_change_cursor; + esm_class->cursor_row = etsm_cursor_row; + esm_class->cursor_col = etsm_cursor_col; + + esm_class->select_single_row = etsm_select_single_row; + esm_class->toggle_single_row = etsm_toggle_single_row; + esm_class->move_selection_end = etsm_move_selection_end; + esm_class->set_selection_end = etsm_set_selection_end; + + g_object_class_install_property ( + object_class, + PROP_CURSOR_ROW, + g_param_spec_int ( + "cursor_row", + "Cursor Row", + NULL, + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_COL, + g_param_spec_int ( + "cursor_col", + "Cursor Column", + NULL, + 0, G_MAXINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MODEL, + g_param_spec_object ( + "model", + "Model", + NULL, + E_TYPE_TREE_MODEL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_ETTA, + g_param_spec_object ( + "etta", + "ETTA", + NULL, + E_TYPE_TREE_TABLE_ADAPTER, + G_PARAM_READWRITE)); + +} + +ESelectionModel * +e_tree_selection_model_new (void) +{ + return g_object_new (E_TYPE_TREE_SELECTION_MODEL, NULL); +} + diff --git a/e-util/e-tree-selection-model.h b/e-util/e-tree-selection-model.h new file mode 100644 index 0000000000..eafa66eba5 --- /dev/null +++ b/e-util/e-tree-selection-model.h @@ -0,0 +1,95 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TREE_SELECTION_MODEL_H_ +#define _E_TREE_SELECTION_MODEL_H_ + +#include <e-util/e-selection-model.h> +#include <e-util/e-sorter.h> +#include <e-util/e-tree-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TREE_SELECTION_MODEL \ + (e_tree_selection_model_get_type ()) +#define E_TREE_SELECTION_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TREE_SELECTION_MODEL, ETreeSelectionModel)) +#define E_TREE_SELECTION_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TREE_SELECTION_MODEL, ETreeSelectionModelClass)) +#define E_IS_TREE_SELECTION_MODEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TREE_SELECTION_MODEL)) +#define E_IS_TREE_SELECTION_MODEL_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TREE_SELECTION_MODEL)) +#define E_TREE_SELECTION_MODEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TREE_SELECTION_MODEL, ETreeSelectionModelClass)) + +G_BEGIN_DECLS + +typedef void (*ETreeForeachFunc) (ETreePath path, + gpointer closure); + +typedef struct _ETreeSelectionModel ETreeSelectionModel; +typedef struct _ETreeSelectionModelClass ETreeSelectionModelClass; +typedef struct _ETreeSelectionModelPrivate ETreeSelectionModelPrivate; + +struct _ETreeSelectionModel { + ESelectionModel parent; + ETreeSelectionModelPrivate *priv; +}; + +struct _ETreeSelectionModelClass { + ESelectionModelClass parent_class; +}; + +GType e_tree_selection_model_get_type (void) G_GNUC_CONST; +ESelectionModel * + e_tree_selection_model_new (void); +void e_tree_selection_model_foreach (ETreeSelectionModel *etsm, + ETreeForeachFunc callback, + gpointer closure); +void e_tree_selection_model_select_single_path + (ETreeSelectionModel *etsm, + ETreePath path); +void e_tree_selection_model_select_paths + (ETreeSelectionModel *etsm, + GPtrArray *paths); + +void e_tree_selection_model_add_to_selection + (ETreeSelectionModel *etsm, + ETreePath path); +void e_tree_selection_model_change_cursor + (ETreeSelectionModel *etsm, + ETreePath path); +ETreePath e_tree_selection_model_get_cursor + (ETreeSelectionModel *etsm); + +G_END_DECLS + +#endif /* _E_TREE_SELECTION_MODEL_H_ */ diff --git a/e-util/e-tree-sorted.c b/e-util/e-tree-sorted.c new file mode 100644 index 0000000000..25cfceb337 --- /dev/null +++ b/e-util/e-tree-sorted.c @@ -0,0 +1,1433 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Chris Toshok <toshok@ximian.com> + * + * Adapted from the gtree code and ETableModel. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* FIXME: Overall e-tree-sorted.c needs to be made more efficient. */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-tree-sorted.h" + +#include <stdio.h> +#include <errno.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> + +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include "e-table-sorting-utils.h" +#include "e-xml-utils.h" + +/* maximum insertions between an idle event that we will do without scheduling an idle sort */ +#define ETS_INSERT_MAX (4) + +#define d(x) + +#define E_TREE_SORTED_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TREE_SORTED, ETreeSortedPrivate)) + +G_DEFINE_TYPE (ETreeSorted, e_tree_sorted, E_TYPE_TREE_MODEL) + +enum { + NODE_RESORTED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0, }; + +typedef struct ETreeSortedPath ETreeSortedPath; + +struct ETreeSortedPath { + ETreePath corresponding; + + /* parent/child/sibling pointers */ + ETreeSortedPath *parent; + gint num_children; + ETreeSortedPath **children; + gint position; + gint orig_position; + + guint needs_resort : 1; + guint child_needs_resort : 1; + guint resort_all_children : 1; + guint needs_regen_to_sort : 1; +}; + +struct _ETreeSortedPrivate { + ETreeModel *source; + ETreeSortedPath *root; + + ETableSortInfo *sort_info; + ETableHeader *full_header; + + ETreeSortedPath *last_access; + + gint tree_model_pre_change_id; + gint tree_model_no_change_id; + gint tree_model_node_changed_id; + gint tree_model_node_data_changed_id; + gint tree_model_node_col_changed_id; + gint tree_model_node_inserted_id; + gint tree_model_node_removed_id; + gint tree_model_node_deleted_id; + gint tree_model_node_request_collapse_id; + + gint sort_info_changed_id; + gint sort_idle_id; + gint insert_idle_id; + gint insert_count; + + guint in_resort_idle : 1; + guint nested_resort_idle : 1; +}; + +enum { + ARG_0, + + ARG_SORT_INFO +}; + +static void ets_sort_info_changed (ETableSortInfo *sort_info, ETreeSorted *ets); +static void resort_node (ETreeSorted *ets, ETreeSortedPath *path, gboolean resort_all_children, gboolean needs_regen, gboolean send_signals); +static void mark_path_needs_resort (ETreeSorted *ets, ETreeSortedPath *path, gboolean needs_rebuild, gboolean resort_all_children); +static void schedule_resort (ETreeSorted *ets, ETreeSortedPath *path, gboolean needs_regen, gboolean resort_all_children); +static void free_path (ETreeSortedPath *path); +static void generate_children (ETreeSorted *ets, ETreeSortedPath *path); +static void regenerate_children (ETreeSorted *ets, ETreeSortedPath *path); + +/* idle callbacks */ + +static gboolean +ets_sort_idle (gpointer user_data) +{ + ETreeSorted *ets = user_data; + if (ets->priv->in_resort_idle) { + ets->priv->nested_resort_idle = TRUE; + return FALSE; + } + ets->priv->in_resort_idle = TRUE; + if (ets->priv->root) { + do { + ets->priv->nested_resort_idle = FALSE; + resort_node (ets, ets->priv->root, FALSE, FALSE, TRUE); + } while (ets->priv->nested_resort_idle); + } + ets->priv->in_resort_idle = FALSE; + ets->priv->sort_idle_id = 0; + return FALSE; +} + +#define ETS_SORT_IDLE_ACTIVATED(ets) ((ets)->priv->sort_idle_id != 0) + +inline static void +ets_stop_sort_idle (ETreeSorted *ets) +{ + if (ets->priv->sort_idle_id) { + g_source_remove (ets->priv->sort_idle_id); + ets->priv->sort_idle_id = 0; + } +} + +static gboolean +ets_insert_idle (ETreeSorted *ets) +{ + ets->priv->insert_count = 0; + ets->priv->insert_idle_id = 0; + return FALSE; +} + +/* Helper functions */ + +#define CHECK_AROUND_LAST_ACCESS + +static inline ETreeSortedPath * +check_last_access (ETreeSorted *ets, + ETreePath corresponding) +{ +#ifdef CHECK_AROUND_LAST_ACCESS + ETreeSortedPath *parent; +#endif + + if (ets->priv->last_access == NULL) + return NULL; + + if (ets->priv->last_access == corresponding) { + d (g_print ("Found last access %p at %p.", ets->priv->last_access, ets->priv->last_access)); + return ets->priv->last_access; + } + +#ifdef CHECK_AROUND_LAST_ACCESS + parent = ets->priv->last_access->parent; + if (parent && parent->children) { + gint position = ets->priv->last_access->position; + gint end = MIN (parent->num_children, position + 10); + gint start = MAX (0, position - 10); + gint initial = MAX (MIN (position, end), start); + gint i; + + for (i = initial; i < end; i++) { + if (parent->children[i] && parent->children[i]->corresponding == corresponding) { + d (g_print ("Found last access %p at %p.", ets->priv->last_access, parent->children[i])); + return parent->children[i]; + } + } + + for (i = initial - 1; i >= start; i--) { + if (parent->children[i] && parent->children[i]->corresponding == corresponding) { + d (g_print ("Found last access %p at %p.", ets->priv->last_access, parent->children[i])); + return parent->children[i]; + } + } + } +#endif + return NULL; +} + +static ETreeSortedPath * +find_path (ETreeSorted *ets, + ETreePath corresponding) +{ + gint depth; + ETreePath *sequence; + gint i; + ETreeSortedPath *path; + ETreeSortedPath *check_last; + + if (corresponding == NULL) + return NULL; + + check_last = check_last_access (ets, corresponding); + if (check_last) { + d (g_print (" (find_path)\n")); + return check_last; + } + + depth = e_tree_model_node_depth (ets->priv->source, corresponding); + + sequence = g_new (ETreePath, depth + 1); + + sequence[0] = corresponding; + + for (i = 0; i < depth; i++) + sequence[i + 1] = e_tree_model_node_get_parent (ets->priv->source, sequence[i]); + + path = ets->priv->root; + + for (i = depth - 1; i >= 0 && path != NULL; i--) { + gint j; + + if (path->num_children == -1) { + path = NULL; + break; + } + + for (j = 0; j < path->num_children; j++) { + if (path->children[j]->corresponding == sequence[i]) { + break; + } + } + + if (j < path->num_children) { + path = path->children[j]; + } else { + path = NULL; + } + } + g_free (sequence); + + d (g_print ("Didn't find last access %p. Setting to %p. (find_path)\n", ets->priv->last_access, path)); + ets->priv->last_access = path; + + return path; +} + +static ETreeSortedPath * +find_child_path (ETreeSorted *ets, + ETreeSortedPath *parent, + ETreePath corresponding) +{ + gint i; + + if (corresponding == NULL) + return NULL; + + if (parent->num_children == -1) { + return NULL; + } + + for (i = 0; i < parent->num_children; i++) + if (parent->children[i]->corresponding == corresponding) + return parent->children[i]; + + return NULL; +} + +static ETreeSortedPath * +find_or_create_path (ETreeSorted *ets, + ETreePath corresponding) +{ + gint depth; + ETreePath *sequence; + gint i; + ETreeSortedPath *path; + ETreeSortedPath *check_last; + + if (corresponding == NULL) + return NULL; + + check_last = check_last_access (ets, corresponding); + if (check_last) { + d (g_print (" (find_or_create_path)\n")); + return check_last; + } + + depth = e_tree_model_node_depth (ets->priv->source, corresponding); + + sequence = g_new (ETreePath, depth + 1); + + sequence[0] = corresponding; + + for (i = 0; i < depth; i++) + sequence[i + 1] = e_tree_model_node_get_parent (ets->priv->source, sequence[i]); + + path = ets->priv->root; + + for (i = depth - 1; i >= 0 && path != NULL; i--) { + gint j; + + if (path->num_children == -1) { + generate_children (ets, path); + } + + for (j = 0; j < path->num_children; j++) { + if (path->children[j]->corresponding == sequence[i]) { + break; + } + } + + if (j < path->num_children) { + path = path->children[j]; + } else { + path = NULL; + } + } + g_free (sequence); + + d (g_print ("Didn't find last access %p. Setting to %p. (find_or_create_path)\n", ets->priv->last_access, path)); + ets->priv->last_access = path; + + return path; +} + +static void +free_children (ETreeSortedPath *path) +{ + gint i; + + if (path == NULL) + return; + + for (i = 0; i < path->num_children; i++) { + free_path (path->children[i]); + } + + g_free (path->children); + path->children = NULL; + path->num_children = -1; +} + +static void +free_path (ETreeSortedPath *path) +{ + free_children (path); + g_slice_free (ETreeSortedPath, path); +} + +static ETreeSortedPath * +new_path (ETreeSortedPath *parent, + ETreePath corresponding) +{ + ETreeSortedPath *path; + + path = g_slice_new0 (ETreeSortedPath); + + path->corresponding = corresponding; + path->parent = parent; + path->num_children = -1; + path->children = NULL; + path->position = -1; + path->orig_position = -1; + path->child_needs_resort = 0; + path->resort_all_children = 0; + path->needs_resort = 0; + path->needs_regen_to_sort = 0; + + return path; +} + +static gboolean +reposition_path (ETreeSorted *ets, + ETreeSortedPath *path) +{ + gint new_index; + gint old_index = path->position; + ETreeSortedPath *parent = path->parent; + gboolean changed = FALSE; + if (parent) { + if (ets->priv->sort_idle_id == 0) { + if (ets->priv->insert_count > ETS_INSERT_MAX) { + /* schedule a sort, and append instead */ + schedule_resort (ets, parent, TRUE, FALSE); + } else { + /* make sure we have an idle handler to reset the count every now and then */ + if (ets->priv->insert_idle_id == 0) { + ets->priv->insert_idle_id = g_idle_add_full (40, (GSourceFunc) ets_insert_idle, ets, NULL); + } + + new_index = e_table_sorting_utils_tree_check_position + (E_TREE_MODEL (ets), + ets->priv->sort_info, + ets->priv->full_header, + (ETreePath *) parent->children, + parent->num_children, + old_index); + + if (new_index > old_index) { + gint i; + ets->priv->insert_count++; + memmove (parent->children + old_index, parent->children + old_index + 1, sizeof (ETreePath) * (new_index - old_index)); + parent->children[new_index] = path; + for (i = old_index; i <= new_index; i++) + parent->children[i]->position = i; + changed = TRUE; + e_tree_model_node_changed (E_TREE_MODEL (ets), parent); + e_tree_sorted_node_resorted (ets, parent); + } else if (new_index < old_index) { + gint i; + ets->priv->insert_count++; + memmove (parent->children + new_index + 1, parent->children + new_index, sizeof (ETreePath) * (old_index - new_index)); + parent->children[new_index] = path; + for (i = new_index; i <= old_index; i++) + parent->children[i]->position = i; + changed = TRUE; + e_tree_model_node_changed (E_TREE_MODEL (ets), parent); + e_tree_sorted_node_resorted (ets, parent); + } + } + } else + mark_path_needs_resort (ets, parent, TRUE, FALSE); + } + return changed; +} + +static void +regenerate_children (ETreeSorted *ets, + ETreeSortedPath *path) +{ + ETreeSortedPath **children; + gint i; + + children = g_new (ETreeSortedPath *, path->num_children); + for (i = 0; i < path->num_children; i++) + children[path->children[i]->orig_position] = path->children[i]; + g_free (path->children); + path->children = children; +} + +static void +generate_children (ETreeSorted *ets, + ETreeSortedPath *path) +{ + ETreePath child; + gint i; + gint count; + + free_children (path); + + count = 0; + for (child = e_tree_model_node_get_first_child (ets->priv->source, path->corresponding); + child; + child = e_tree_model_node_get_next (ets->priv->source, child)) { + count++; + } + + path->num_children = count; + path->children = g_new (ETreeSortedPath *, count); + for (child = e_tree_model_node_get_first_child (ets->priv->source, path->corresponding), i = 0; + child; + child = e_tree_model_node_get_next (ets->priv->source, child), i++) { + path->children[i] = new_path (path, child); + path->children[i]->position = i; + path->children[i]->orig_position = i; + } + if (path->num_children > 0) + schedule_resort (ets, path, FALSE, TRUE); +} + +static void +resort_node (ETreeSorted *ets, + ETreeSortedPath *path, + gboolean resort_all_children, + gboolean needs_regen, + gboolean send_signals) +{ + gboolean needs_resort; + if (path) { + needs_resort = path->needs_resort || resort_all_children; + needs_regen = path->needs_regen_to_sort || needs_regen; + if (path->num_children > 0) { + if (needs_resort && send_signals) + e_tree_model_pre_change (E_TREE_MODEL (ets)); + if (needs_resort) { + gint i; + d (g_print ("Start sort of node %p\n", path)); + if (needs_regen) + regenerate_children (ets, path); + d (g_print ("Regened sort of node %p\n", path)); + e_table_sorting_utils_tree_sort ( + E_TREE_MODEL (ets), + ets->priv->sort_info, + ets->priv->full_header, + (ETreePath *) path->children, + path->num_children); + d (g_print ("Renumbering sort of node %p\n", path)); + for (i = 0; i < path->num_children; i++) { + path->children[i]->position = i; + } + d (g_print ("End sort of node %p\n", path)); + } + if (path->resort_all_children) + resort_all_children = TRUE; + if ((resort_all_children || path->child_needs_resort) && path->num_children >= 0) { + gint i; + for (i = 0; i < path->num_children; i++) { + resort_node (ets, path->children[i], resort_all_children, needs_regen, send_signals && !needs_resort); + } + path->child_needs_resort = 0; + } + } + path->needs_resort = 0; + path->child_needs_resort = 0; + path->needs_regen_to_sort = 0; + path->resort_all_children = 0; + if (needs_resort && send_signals && path->num_children > 0) { + e_tree_model_node_changed (E_TREE_MODEL (ets), path); + e_tree_sorted_node_resorted (ets, path); + } + } +} + +static void +mark_path_child_needs_resort (ETreeSorted *ets, + ETreeSortedPath *path) +{ + if (path == NULL) + return; + if (!path->child_needs_resort) { + path->child_needs_resort = 1; + mark_path_child_needs_resort (ets, path->parent); + } +} + +static void +mark_path_needs_resort (ETreeSorted *ets, + ETreeSortedPath *path, + gboolean needs_regen, + gboolean resort_all_children) +{ + if (path == NULL) + return; + if (path->num_children == 0) + return; + path->needs_resort = 1; + path->needs_regen_to_sort = needs_regen; + path->resort_all_children = resort_all_children; + mark_path_child_needs_resort (ets, path->parent); +} + +static void +schedule_resort (ETreeSorted *ets, + ETreeSortedPath *path, + gboolean needs_regen, + gboolean resort_all_children) +{ + ets->priv->insert_count = 0; + if (ets->priv->insert_idle_id != 0) { + g_source_remove (ets->priv->insert_idle_id); + ets->priv->insert_idle_id = 0; + } + + if (path == NULL) + return; + if (path->num_children == 0) + return; + + mark_path_needs_resort (ets, path, needs_regen, resort_all_children); + if (ets->priv->sort_idle_id == 0) { + ets->priv->sort_idle_id = g_idle_add_full (50, (GSourceFunc) ets_sort_idle, ets, NULL); + } else if (ets->priv->in_resort_idle) { + ets->priv->nested_resort_idle = TRUE; + } +} + +/* virtual methods */ + +static void +ets_dispose (GObject *object) +{ + ETreeSortedPrivate *priv; + + priv = E_TREE_SORTED_GET_PRIVATE (object); + + if (priv->source) { + g_signal_handler_disconnect ( + priv->source, priv->tree_model_pre_change_id); + g_signal_handler_disconnect ( + priv->source, priv->tree_model_no_change_id); + g_signal_handler_disconnect ( + priv->source, priv->tree_model_node_changed_id); + g_signal_handler_disconnect ( + priv->source, priv->tree_model_node_data_changed_id); + g_signal_handler_disconnect ( + priv->source, priv->tree_model_node_col_changed_id); + g_signal_handler_disconnect ( + priv->source, priv->tree_model_node_inserted_id); + g_signal_handler_disconnect ( + priv->source, priv->tree_model_node_removed_id); + g_signal_handler_disconnect ( + priv->source, priv->tree_model_node_deleted_id); + g_signal_handler_disconnect ( + priv->source, priv->tree_model_node_request_collapse_id); + + g_object_unref (priv->source); + priv->source = NULL; + + priv->tree_model_pre_change_id = 0; + priv->tree_model_no_change_id = 0; + priv->tree_model_node_changed_id = 0; + priv->tree_model_node_data_changed_id = 0; + priv->tree_model_node_col_changed_id = 0; + priv->tree_model_node_inserted_id = 0; + priv->tree_model_node_removed_id = 0; + priv->tree_model_node_deleted_id = 0; + priv->tree_model_node_request_collapse_id = 0; + } + + if (priv->sort_info) { + g_signal_handler_disconnect ( + priv->sort_info, priv->sort_info_changed_id); + priv->sort_info_changed_id = 0; + + g_object_unref (priv->sort_info); + priv->sort_info = NULL; + } + + ets_stop_sort_idle (E_TREE_SORTED (object)); + + if (priv->insert_idle_id) { + g_source_remove (priv->insert_idle_id); + priv->insert_idle_id = 0; + } + + if (priv->full_header) { + g_object_unref (priv->full_header); + priv->full_header = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_tree_sorted_parent_class)->dispose (object); +} + +static void +ets_finalize (GObject *object) +{ + ETreeSortedPrivate *priv; + + priv = E_TREE_SORTED_GET_PRIVATE (object); + + if (priv->root) + free_path (priv->root); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_tree_sorted_parent_class)->finalize (object); +} + +static ETreePath +ets_get_root (ETreeModel *etm) +{ + ETreeSortedPrivate *priv = E_TREE_SORTED (etm)->priv; + if (priv->root == NULL) { + ETreeSorted *ets = E_TREE_SORTED (etm); + ETreePath corresponding = e_tree_model_get_root (ets->priv->source); + + if (corresponding) { + priv->root = new_path (NULL, corresponding); + } + } + if (priv->root && priv->root->num_children == -1) { + generate_children (E_TREE_SORTED (etm), priv->root); + } + + return priv->root; +} + +static ETreePath +ets_get_parent (ETreeModel *etm, + ETreePath node) +{ + ETreeSortedPath *path = node; + return path->parent; +} + +static ETreePath +ets_get_first_child (ETreeModel *etm, + ETreePath node) +{ + ETreeSortedPath *path = node; + ETreeSorted *ets = E_TREE_SORTED (etm); + + if (path->num_children == -1) + generate_children (ets, path); + + if (path->num_children > 0) + return path->children[0]; + else + return NULL; +} + +static ETreePath +ets_get_last_child (ETreeModel *etm, + ETreePath node) +{ + ETreeSortedPath *path = node; + ETreeSorted *ets = E_TREE_SORTED (etm); + + if (path->num_children == -1) + generate_children (ets, path); + + if (path->num_children > 0) + return path->children[path->num_children - 1]; + else + return NULL; +} + +static ETreePath +ets_get_next (ETreeModel *etm, + ETreePath node) +{ + ETreeSortedPath *path = node; + ETreeSortedPath *parent = path->parent; + if (parent) { + if (parent->num_children > path->position + 1) + return parent->children[path->position + 1]; + else + return NULL; + } else + return NULL; +} + +static ETreePath +ets_get_prev (ETreeModel *etm, + ETreePath node) +{ + ETreeSortedPath *path = node; + ETreeSortedPath *parent = path->parent; + if (parent) { + if (path->position - 1 >= 0) + return parent->children[path->position - 1]; + else + return NULL; + } else + return NULL; +} + +static gboolean +ets_is_root (ETreeModel *etm, + ETreePath node) +{ + ETreeSortedPath *path = node; + ETreeSorted *ets = E_TREE_SORTED (etm); + + return e_tree_model_node_is_root (ets->priv->source, path->corresponding); +} + +static gboolean +ets_is_expandable (ETreeModel *etm, + ETreePath node) +{ + ETreeSortedPath *path = node; + ETreeSorted *ets = E_TREE_SORTED (etm); + gboolean expandable = e_tree_model_node_is_expandable (ets->priv->source, path->corresponding); + + if (path->num_children == -1) { + generate_children (ets, node); + } + + return expandable; +} + +static guint +ets_get_children (ETreeModel *etm, + ETreePath node, + ETreePath **nodes) +{ + ETreeSortedPath *path = node; + guint n_children; + + if (path->num_children == -1) { + generate_children (E_TREE_SORTED (etm), node); + } + + n_children = path->num_children; + + if (nodes) { + gint i; + + (*nodes) = g_malloc (sizeof (ETreePath) * n_children); + for (i = 0; i < n_children; i++) { + (*nodes)[i] = path->children[i]; + } + } + + return n_children; +} + +static guint +ets_depth (ETreeModel *etm, + ETreePath node) +{ + ETreeSortedPath *path = node; + ETreeSorted *ets = E_TREE_SORTED (etm); + + return e_tree_model_node_depth (ets->priv->source, path->corresponding); +} + +static GdkPixbuf * +ets_icon_at (ETreeModel *etm, + ETreePath node) +{ + ETreeSortedPath *path = node; + ETreeSorted *ets = E_TREE_SORTED (etm); + + return e_tree_model_icon_at (ets->priv->source, path->corresponding); +} + +static gboolean +ets_get_expanded_default (ETreeModel *etm) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + + return e_tree_model_get_expanded_default (ets->priv->source); +} + +static gint +ets_column_count (ETreeModel *etm) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + + return e_tree_model_column_count (ets->priv->source); +} + +static gboolean +ets_has_save_id (ETreeModel *etm) +{ + return TRUE; +} + +static gchar * +ets_get_save_id (ETreeModel *etm, + ETreePath node) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + ETreeSortedPath *path = node; + + if (e_tree_model_has_save_id (ets->priv->source)) + return e_tree_model_get_save_id (ets->priv->source, path->corresponding); + else + return g_strdup_printf ("%p", path->corresponding); +} + +static gboolean +ets_has_get_node_by_id (ETreeModel *etm) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + return e_tree_model_has_get_node_by_id (ets->priv->source); +} + +static ETreePath +ets_get_node_by_id (ETreeModel *etm, + const gchar *save_id) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + ETreePath node; + + node = e_tree_model_get_node_by_id (ets->priv->source, save_id); + + return find_path (ets, node); +} + +static gboolean +ets_has_change_pending (ETreeModel *etm) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + + return ets->priv->sort_idle_id != 0; +} + +static gpointer +ets_value_at (ETreeModel *etm, + ETreePath node, + gint col) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + ETreeSortedPath *path = node; + + return e_tree_model_value_at (ets->priv->source, path->corresponding, col); +} + +static void +ets_set_value_at (ETreeModel *etm, + ETreePath node, + gint col, + gconstpointer val) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + ETreeSortedPath *path = node; + + e_tree_model_set_value_at (ets->priv->source, path->corresponding, col, val); +} + +static gboolean +ets_is_editable (ETreeModel *etm, + ETreePath node, + gint col) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + ETreeSortedPath *path = node; + + return e_tree_model_node_is_editable (ets->priv->source, path->corresponding, col); +} + +/* The default for ets_duplicate_value is to return the raw value. */ +static gpointer +ets_duplicate_value (ETreeModel *etm, + gint col, + gconstpointer value) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + + return e_tree_model_duplicate_value (ets->priv->source, col, value); +} + +static void +ets_free_value (ETreeModel *etm, + gint col, + gpointer value) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + + e_tree_model_free_value (ets->priv->source, col, value); +} + +static gpointer +ets_initialize_value (ETreeModel *etm, + gint col) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + + return e_tree_model_initialize_value (ets->priv->source, col); +} + +static gboolean +ets_value_is_empty (ETreeModel *etm, + gint col, + gconstpointer value) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + + return e_tree_model_value_is_empty (ets->priv->source, col, value); +} + +static gchar * +ets_value_to_string (ETreeModel *etm, + gint col, + gconstpointer value) +{ + ETreeSorted *ets = E_TREE_SORTED (etm); + + return e_tree_model_value_to_string (ets->priv->source, col, value); +} + +/* Proxy functions */ + +static void +ets_proxy_pre_change (ETreeModel *etm, + ETreeSorted *ets) +{ + e_tree_model_pre_change (E_TREE_MODEL (ets)); +} + +static void +ets_proxy_no_change (ETreeModel *etm, + ETreeSorted *ets) +{ + e_tree_model_no_change (E_TREE_MODEL (ets)); +} + +static void +ets_proxy_node_changed (ETreeModel *etm, + ETreePath node, + ETreeSorted *ets) +{ + ets->priv->last_access = NULL; + d (g_print ("Setting last access %p. (ets_proxy_node_changed)\n", ets->priv->last_access)); + + if (e_tree_model_node_is_root (ets->priv->source, node)) { + ets_stop_sort_idle (ets); + + if (ets->priv->root) { + free_path (ets->priv->root); + } + ets->priv->root = new_path (NULL, node); + e_tree_model_node_changed (E_TREE_MODEL (ets), ets->priv->root); + return; + } else { + ETreeSortedPath *path = find_path (ets, node); + + if (path) { + free_children (path); + if (!reposition_path (ets, path)) { + e_tree_model_node_changed (E_TREE_MODEL (ets), path); + } else { + e_tree_model_no_change (E_TREE_MODEL (ets)); + } + } else { + e_tree_model_no_change (E_TREE_MODEL (ets)); + } + } +} + +static void +ets_proxy_node_data_changed (ETreeModel *etm, + ETreePath node, + ETreeSorted *ets) +{ + ETreeSortedPath *path = find_path (ets, node); + + if (path) { + if (!reposition_path (ets, path)) + e_tree_model_node_data_changed (E_TREE_MODEL (ets), path); + else + e_tree_model_no_change (E_TREE_MODEL (ets)); + } else + e_tree_model_no_change (E_TREE_MODEL (ets)); +} + +static void +ets_proxy_node_col_changed (ETreeModel *etm, + ETreePath node, + gint col, + ETreeSorted *ets) +{ + ETreeSortedPath *path = find_path (ets, node); + + if (path) { + gboolean changed = FALSE; + if (e_table_sorting_utils_affects_sort (ets->priv->sort_info, ets->priv->full_header, col)) + changed = reposition_path (ets, path); + if (!changed) + e_tree_model_node_col_changed (E_TREE_MODEL (ets), path, col); + else + e_tree_model_no_change (E_TREE_MODEL (ets)); + } else + e_tree_model_no_change (E_TREE_MODEL (ets)); +} + +static void +ets_proxy_node_inserted (ETreeModel *etm, + ETreePath parent, + ETreePath child, + ETreeSorted *ets) +{ + ETreeSortedPath *parent_path = find_path (ets, parent); + + if (parent_path && parent_path->num_children != -1) { + gint i; + gint j; + ETreeSortedPath *path; + gint position = parent_path->num_children; + ETreePath counter; + + for (counter = e_tree_model_node_get_next (etm, child); + counter; + counter = e_tree_model_node_get_next (etm, counter)) + position--; + + if (position != parent_path->num_children) { + for (i = 0; i < parent_path->num_children; i++) { + if (parent_path->children[i]->orig_position >= position) + parent_path->children[i]->orig_position++; + } + } + + i = parent_path->num_children; + path = new_path (parent_path, child); + path->orig_position = position; + if (!ETS_SORT_IDLE_ACTIVATED (ets)) { + ets->priv->insert_count++; + if (ets->priv->insert_count > ETS_INSERT_MAX) { + /* schedule a sort, and append instead */ + schedule_resort (ets, parent_path, TRUE, FALSE); + } else { + /* make sure we have an idle handler to reset the count every now and then */ + if (ets->priv->insert_idle_id == 0) { + ets->priv->insert_idle_id = g_idle_add_full (40, (GSourceFunc) ets_insert_idle, ets, NULL); + } + i = e_table_sorting_utils_tree_insert + (ets->priv->source, + ets->priv->sort_info, + ets->priv->full_header, + (ETreePath *) parent_path->children, + parent_path->num_children, + path); + } + } else { + mark_path_needs_resort (ets, parent_path, TRUE, FALSE); + } + parent_path->num_children++; + parent_path->children = g_renew (ETreeSortedPath *, parent_path->children, parent_path->num_children); + memmove (parent_path->children + i + 1, parent_path->children + i, (parent_path->num_children - 1 - i) * sizeof (gint)); + parent_path->children[i] = path; + for (j = i; j < parent_path->num_children; j++) { + parent_path->children[j]->position = j; + } + e_tree_model_node_inserted (E_TREE_MODEL (ets), parent_path, parent_path->children[i]); + } else if (ets->priv->root == NULL && parent == NULL) { + if (child) { + ets->priv->root = new_path (NULL, child); + e_tree_model_node_inserted (E_TREE_MODEL (ets), NULL, ets->priv->root); + } else { + e_tree_model_no_change (E_TREE_MODEL (ets)); + } + } else { + e_tree_model_no_change (E_TREE_MODEL (ets)); + } +} + +static void +ets_proxy_node_removed (ETreeModel *etm, + ETreePath parent, + ETreePath child, + gint old_position, + ETreeSorted *ets) +{ + ETreeSortedPath *parent_path = find_path (ets, parent); + ETreeSortedPath *path; + + if (parent_path) + path = find_child_path (ets, parent_path, child); + else + path = find_path (ets, child); + + d (g_print ("Setting last access %p. (ets_proxy_node_removed)\n ", ets->priv->last_access)); + ets->priv->last_access = NULL; + + if (path && parent_path && parent_path->num_children != -1) { + gint i; + for (i = 0; i < parent_path->num_children; i++) { + if (parent_path->children[i]->orig_position > old_position) + parent_path->children[i]->orig_position--; + } + + i = path->position; + + parent_path->num_children--; + memmove (parent_path->children + i, parent_path->children + i + 1, sizeof (ETreeSortedPath *) * (parent_path->num_children - i)); + for (; i < parent_path->num_children; i++) { + parent_path->children[i]->position = i; + } + e_tree_model_node_removed (E_TREE_MODEL (ets), parent_path, path, path->position); + free_path (path); + } else if (path && path == ets->priv->root) { + ets->priv->root = NULL; + e_tree_model_node_removed (E_TREE_MODEL (ets), NULL, path, -1); + free_path (path); + } +} + +static void +ets_proxy_node_deleted (ETreeModel *etm, + ETreePath child, + ETreeSorted *ets) +{ + e_tree_model_node_deleted (E_TREE_MODEL (ets), NULL); +} + +static void +ets_proxy_node_request_collapse (ETreeModel *etm, + ETreePath node, + ETreeSorted *ets) +{ + ETreeSortedPath *path = find_path (ets, node); + if (path) { + e_tree_model_node_request_collapse (E_TREE_MODEL (ets), path); + } +} + +static void +ets_sort_info_changed (ETableSortInfo *sort_info, + ETreeSorted *ets) +{ + schedule_resort (ets, ets->priv->root, TRUE, TRUE); +} + +/* Initialization and creation */ + +static void +e_tree_sorted_class_init (ETreeSortedClass *class) +{ + GObjectClass *object_class; + ETreeModelClass *tree_model_class; + + g_type_class_add_private (class, sizeof (ETreeSortedPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = ets_dispose; + object_class->finalize = ets_finalize; + + tree_model_class = E_TREE_MODEL_CLASS (class); + tree_model_class->get_root = ets_get_root; + tree_model_class->get_parent = ets_get_parent; + tree_model_class->get_first_child = ets_get_first_child; + tree_model_class->get_last_child = ets_get_last_child; + tree_model_class->get_prev = ets_get_prev; + tree_model_class->get_next = ets_get_next; + + tree_model_class->is_root = ets_is_root; + tree_model_class->is_expandable = ets_is_expandable; + tree_model_class->get_children = ets_get_children; + tree_model_class->depth = ets_depth; + + tree_model_class->icon_at = ets_icon_at; + + tree_model_class->get_expanded_default = ets_get_expanded_default; + tree_model_class->column_count = ets_column_count; + + tree_model_class->has_save_id = ets_has_save_id; + tree_model_class->get_save_id = ets_get_save_id; + + tree_model_class->has_get_node_by_id = ets_has_get_node_by_id; + tree_model_class->get_node_by_id = ets_get_node_by_id; + + tree_model_class->has_change_pending = ets_has_change_pending; + + tree_model_class->value_at = ets_value_at; + tree_model_class->set_value_at = ets_set_value_at; + tree_model_class->is_editable = ets_is_editable; + + tree_model_class->duplicate_value = ets_duplicate_value; + tree_model_class->free_value = ets_free_value; + tree_model_class->initialize_value = ets_initialize_value; + tree_model_class->value_is_empty = ets_value_is_empty; + tree_model_class->value_to_string = ets_value_to_string; + + signals[NODE_RESORTED] = g_signal_new ( + "node_resorted", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeSortedClass, node_resorted), + (GSignalAccumulator) NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); +} + +static void +e_tree_sorted_init (ETreeSorted *ets) +{ + ets->priv = E_TREE_SORTED_GET_PRIVATE (ets); +} + +/** + * e_tree_sorted_construct: + * @etree: + * + * + **/ +void +e_tree_sorted_construct (ETreeSorted *ets, + ETreeModel *source, + ETableHeader *full_header, + ETableSortInfo *sort_info) +{ + ets->priv->source = source; + if (source) + g_object_ref (source); + + ets->priv->full_header = full_header; + if (full_header) + g_object_ref (full_header); + + e_tree_sorted_set_sort_info (ets, sort_info); + + ets->priv->tree_model_pre_change_id = g_signal_connect ( + source, "pre_change", + G_CALLBACK (ets_proxy_pre_change), ets); + ets->priv->tree_model_no_change_id = g_signal_connect ( + source, "no_change", + G_CALLBACK (ets_proxy_no_change), ets); + ets->priv->tree_model_node_changed_id = g_signal_connect ( + source, "node_changed", + G_CALLBACK (ets_proxy_node_changed), ets); + ets->priv->tree_model_node_data_changed_id = g_signal_connect ( + source, "node_data_changed", + G_CALLBACK (ets_proxy_node_data_changed), ets); + ets->priv->tree_model_node_col_changed_id = g_signal_connect ( + source, "node_col_changed", + G_CALLBACK (ets_proxy_node_col_changed), ets); + ets->priv->tree_model_node_inserted_id = g_signal_connect ( + source, "node_inserted", + G_CALLBACK (ets_proxy_node_inserted), ets); + ets->priv->tree_model_node_removed_id = g_signal_connect ( + source, "node_removed", + G_CALLBACK (ets_proxy_node_removed), ets); + ets->priv->tree_model_node_deleted_id = g_signal_connect ( + source, "node_deleted", + G_CALLBACK (ets_proxy_node_deleted), ets); + ets->priv->tree_model_node_request_collapse_id = g_signal_connect ( + source, "node_request_collapse", + G_CALLBACK (ets_proxy_node_request_collapse), ets); + +} + +/** + * e_tree_sorted_new + * + * FIXME docs here. + * + * return values: a newly constructed ETreeSorted. + */ +ETreeSorted * +e_tree_sorted_new (ETreeModel *source, + ETableHeader *full_header, + ETableSortInfo *sort_info) +{ + ETreeSorted *ets = g_object_new (E_TYPE_TREE_SORTED, NULL); + + e_tree_sorted_construct (ets, source, full_header, sort_info); + + return ets; +} + +ETreePath +e_tree_sorted_view_to_model_path (ETreeSorted *ets, + ETreePath view_path) +{ + ETreeSortedPath *path = view_path; + if (path) { + ets->priv->last_access = path; + d (g_print ("Setting last access %p. (e_tree_sorted_view_to_model_path)\n", ets->priv->last_access)); + return path->corresponding; + } else + return NULL; +} + +ETreePath +e_tree_sorted_model_to_view_path (ETreeSorted *ets, + ETreePath model_path) +{ + return find_or_create_path (ets, model_path); +} + +gint +e_tree_sorted_orig_position (ETreeSorted *ets, + ETreePath path) +{ + ETreeSortedPath *sorted_path = path; + return sorted_path->orig_position; +} + +gint +e_tree_sorted_node_num_children (ETreeSorted *ets, + ETreePath path) +{ + ETreeSortedPath *sorted_path = path; + + if (sorted_path->num_children == -1) { + generate_children (ets, sorted_path); + } + + return sorted_path->num_children; +} + +void +e_tree_sorted_node_resorted (ETreeSorted *sorted, + ETreePath node) +{ + g_return_if_fail (sorted != NULL); + g_return_if_fail (E_IS_TREE_SORTED (sorted)); + + g_signal_emit (sorted, signals[NODE_RESORTED], 0, node); +} + +void +e_tree_sorted_set_sort_info (ETreeSorted *ets, + ETableSortInfo *sort_info) +{ + + g_return_if_fail (ets != NULL); + + if (ets->priv->sort_info) { + if (ets->priv->sort_info_changed_id != 0) + g_signal_handler_disconnect ( + ets->priv->sort_info, + ets->priv->sort_info_changed_id); + ets->priv->sort_info_changed_id = 0; + g_object_unref (ets->priv->sort_info); + } + + ets->priv->sort_info = sort_info; + if (sort_info) { + g_object_ref (sort_info); + ets->priv->sort_info_changed_id = g_signal_connect ( + ets->priv->sort_info, "sort_info_changed", + G_CALLBACK (ets_sort_info_changed), ets); + } + + if (ets->priv->root) + schedule_resort (ets, ets->priv->root, TRUE, TRUE); +} + +ETableSortInfo * +e_tree_sorted_get_sort_info (ETreeSorted *ets) +{ + return ets->priv->sort_info; +} + diff --git a/e-util/e-tree-sorted.h b/e-util/e-tree-sorted.h new file mode 100644 index 0000000000..b6dacaf9d6 --- /dev/null +++ b/e-util/e-tree-sorted.h @@ -0,0 +1,104 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TREE_SORTED_H_ +#define _E_TREE_SORTED_H_ + +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include <e-util/e-table-header.h> +#include <e-util/e-table-sort-info.h> +#include <e-util/e-tree-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TREE_SORTED \ + (e_tree_sorted_get_type ()) +#define E_TREE_SORTED(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TREE_SORTED, ETreeSorted)) +#define E_TREE_SORTED_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TREE_SORTED, ETreeSortedClass)) +#define E_IS_TREE_SORTED(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TREE_SORTED)) +#define E_IS_TREE_SORTED_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TREE_SORTED)) +#define E_TREE_SORTED_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TREE_SORTED, ETreeSortedClass)) + +G_BEGIN_DECLS + +typedef struct _ETreeSorted ETreeSorted; +typedef struct _ETreeSortedClass ETreeSortedClass; +typedef struct _ETreeSortedPrivate ETreeSortedPrivate; + +struct _ETreeSorted { + ETreeModel parent; + ETreeSortedPrivate *priv; +}; + +struct _ETreeSortedClass { + ETreeModelClass parent_class; + + /* Signals */ + void (*node_resorted) (ETreeSorted *etm, + ETreePath node); +}; + +GType e_tree_sorted_get_type (void) G_GNUC_CONST; +void e_tree_sorted_construct (ETreeSorted *etree, + ETreeModel *source, + ETableHeader *full_header, + ETableSortInfo *sort_info); +ETreeSorted * e_tree_sorted_new (ETreeModel *source, + ETableHeader *full_header, + ETableSortInfo *sort_info); + +ETreePath e_tree_sorted_view_to_model_path + (ETreeSorted *ets, + ETreePath view_path); +ETreePath e_tree_sorted_model_to_view_path + (ETreeSorted *ets, + ETreePath model_path); +gint e_tree_sorted_orig_position (ETreeSorted *ets, + ETreePath path); +gint e_tree_sorted_node_num_children (ETreeSorted *ets, + ETreePath path); + +void e_tree_sorted_node_resorted (ETreeSorted *tree_model, + ETreePath node); + +ETableSortInfo *e_tree_sorted_get_sort_info (ETreeSorted *tree_model); +void e_tree_sorted_set_sort_info (ETreeSorted *tree_model, + ETableSortInfo *sort_info); + +G_END_DECLS + +#endif /* _E_TREE_SORTED_H */ diff --git a/e-util/e-tree-table-adapter.c b/e-util/e-tree-table-adapter.c new file mode 100644 index 0000000000..f76f11b26a --- /dev/null +++ b/e-util/e-tree-table-adapter.c @@ -0,0 +1,1414 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Chris Toshok <toshok@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-tree-table-adapter.h" + +#include <stdlib.h> +#include <string.h> + +#include <glib/gstdio.h> + +#include <libxml/tree.h> +#include <libxml/parser.h> + +#include <libedataserver/libedataserver.h> + +#include "e-marshal.h" +#include "e-table-sorting-utils.h" +#include "e-xml-utils.h" + +#define E_TREE_TABLE_ADAPTER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TREE_TABLE_ADAPTER, ETreeTableAdapterPrivate)) + +/* workaround for avoiding API breakage */ +#define etta_get_type e_tree_table_adapter_get_type +G_DEFINE_TYPE (ETreeTableAdapter, etta, E_TYPE_TABLE_MODEL) +#define d(x) + +#define INCREMENT_AMOUNT 100 + +enum { + SORTING_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +typedef struct { + ETreePath path; + guint32 num_visible_children; + guint32 index; + + guint expanded : 1; + guint expandable : 1; + guint expandable_set : 1; +} node_t; + +struct _ETreeTableAdapterPrivate { + ETreeModel *source; + ETableSortInfo *sort_info; + ETableHeader *header; + + gint n_map; + gint n_vals_allocated; + node_t **map_table; + GHashTable *nodes; + GNode *root; + + guint root_visible : 1; + guint remap_needed : 1; + + gint last_access; + + gint pre_change_id; + gint no_change_id; + gint rebuilt_id; + gint node_changed_id; + gint node_data_changed_id; + gint node_col_changed_id; + gint node_inserted_id; + gint node_removed_id; + gint node_request_collapse_id; + gint sort_info_changed_id; + + guint resort_idle_id; + + gint force_expanded_state; /* use this instead of model's default if not 0; <0 ... collapse, >0 ... expand */ +}; + +static void etta_sort_info_changed (ETableSortInfo *sort_info, ETreeTableAdapter *etta); + +static GNode * +lookup_gnode (ETreeTableAdapter *etta, + ETreePath path) +{ + GNode *gnode; + + if (!path) + return NULL; + + gnode = g_hash_table_lookup (etta->priv->nodes, path); + + return gnode; +} + +static void +resize_map (ETreeTableAdapter *etta, + gint size) +{ + if (size > etta->priv->n_vals_allocated) { + etta->priv->n_vals_allocated = MAX (etta->priv->n_vals_allocated + INCREMENT_AMOUNT, size); + etta->priv->map_table = g_renew (node_t *, etta->priv->map_table, etta->priv->n_vals_allocated); + } + + etta->priv->n_map = size; +} + +static void +move_map_elements (ETreeTableAdapter *etta, + gint to, + gint from, + gint count) +{ + if (count <= 0 || from >= etta->priv->n_map) + return; + memmove (etta->priv->map_table + to, etta->priv->map_table + from, count * sizeof (node_t *)); + etta->priv->remap_needed = TRUE; +} + +static gint +fill_map (ETreeTableAdapter *etta, + gint index, + GNode *gnode) +{ + GNode *p; + + if ((gnode != etta->priv->root) || etta->priv->root_visible) + etta->priv->map_table[index++] = gnode->data; + + for (p = gnode->children; p; p = p->next) + index = fill_map (etta, index, p); + + etta->priv->remap_needed = TRUE; + return index; +} + +static void +remap_indices (ETreeTableAdapter *etta) +{ + gint i; + for (i = 0; i < etta->priv->n_map; i++) + etta->priv->map_table[i]->index = i; + etta->priv->remap_needed = FALSE; +} + +static node_t * +get_node (ETreeTableAdapter *etta, + ETreePath path) +{ + GNode *gnode = lookup_gnode (etta, path); + + if (!gnode) + return NULL; + + return (node_t *) gnode->data; +} + +static void +resort_node (ETreeTableAdapter *etta, + GNode *gnode, + gboolean recurse) +{ + node_t *node = (node_t *) gnode->data; + ETreePath *paths, path; + GNode *prev, *curr; + gint i, count; + gboolean sort_needed; + + if (node->num_visible_children == 0) + return; + + sort_needed = etta->priv->sort_info && e_table_sort_info_sorting_get_count (etta->priv->sort_info) > 0; + + for (i = 0, path = e_tree_model_node_get_first_child (etta->priv->source, node->path); path; + path = e_tree_model_node_get_next (etta->priv->source, path), i++); + + count = i; + if (count <= 1) + return; + + paths = g_new0 (ETreePath, count); + + for (i = 0, path = e_tree_model_node_get_first_child (etta->priv->source, node->path); path; + path = e_tree_model_node_get_next (etta->priv->source, path), i++) + paths[i] = path; + + if (count > 1 && sort_needed) + e_table_sorting_utils_tree_sort (etta->priv->source, etta->priv->sort_info, etta->priv->header, paths, count); + + prev = NULL; + for (i = 0; i < count; i++) { + curr = lookup_gnode (etta, paths[i]); + if (!curr) + continue; + + if (prev) + prev->next = curr; + else + gnode->children = curr; + + curr->prev = prev; + curr->next = NULL; + prev = curr; + if (recurse) + resort_node (etta, curr, recurse); + } + + g_free (paths); +} + +static gint +get_row (ETreeTableAdapter *etta, + ETreePath path) +{ + node_t *node = get_node (etta, path); + if (!node) + return -1; + + if (etta->priv->remap_needed) + remap_indices (etta); + + return node->index; +} + +static ETreePath +get_path (ETreeTableAdapter *etta, + gint row) +{ + if (row == -1 && etta->priv->n_map > 0) + row = etta->priv->n_map - 1; + else if (row < 0 || row >= etta->priv->n_map) + return NULL; + + return etta->priv->map_table[row]->path; +} + +static void +kill_gnode (GNode *node, + ETreeTableAdapter *etta) +{ + g_hash_table_remove (etta->priv->nodes, ((node_t *) node->data)->path); + + while (node->children) { + GNode *next = node->children->next; + kill_gnode (node->children, etta); + node->children = next; + } + + g_free (node->data); + if (node == etta->priv->root) + etta->priv->root = NULL; + g_node_destroy (node); +} + +static void +update_child_counts (GNode *gnode, + gint delta) +{ + while (gnode) { + node_t *node = (node_t *) gnode->data; + node->num_visible_children += delta; + gnode = gnode->parent; + } +} + +static gint +delete_children (ETreeTableAdapter *etta, + GNode *gnode) +{ + node_t *node = (node_t *) gnode->data; + gint to_remove = node ? node->num_visible_children : 0; + + if (to_remove == 0) + return 0; + + while (gnode->children) { + GNode *next = gnode->children->next; + kill_gnode (gnode->children, etta); + gnode->children = next; + } + + return to_remove; +} + +static void +delete_node (ETreeTableAdapter *etta, + ETreePath parent, + ETreePath path) +{ + gint to_remove = 1; + gint parent_row = get_row (etta, parent); + gint row = get_row (etta, path); + GNode *gnode = lookup_gnode (etta, path); + GNode *parent_gnode = lookup_gnode (etta, parent); + + e_table_model_pre_change (E_TABLE_MODEL (etta)); + + if (row == -1) { + e_table_model_no_change (E_TABLE_MODEL (etta)); + return; + } + + to_remove += delete_children (etta, gnode); + kill_gnode (gnode, etta); + + move_map_elements (etta, row, row + to_remove, etta->priv->n_map - row - to_remove); + resize_map (etta, etta->priv->n_map - to_remove); + + if (parent_gnode != NULL) { + node_t *parent_node = parent_gnode->data; + gboolean expandable = e_tree_model_node_is_expandable (etta->priv->source, parent); + + update_child_counts (parent_gnode, - to_remove); + if (parent_node->expandable != expandable) { + e_table_model_pre_change (E_TABLE_MODEL (etta)); + parent_node->expandable = expandable; + e_table_model_row_changed (E_TABLE_MODEL (etta), parent_row); + } + + resort_node (etta, parent_gnode, FALSE); + } + + e_table_model_rows_deleted (E_TABLE_MODEL (etta), row, to_remove); +} + +static GNode * +create_gnode (ETreeTableAdapter *etta, + ETreePath path) +{ + GNode *gnode; + node_t *node; + + node = g_new0 (node_t, 1); + node->path = path; + node->index = -1; + node->expanded = etta->priv->force_expanded_state == 0 ? e_tree_model_get_expanded_default (etta->priv->source) : etta->priv->force_expanded_state > 0; + node->expandable = e_tree_model_node_is_expandable (etta->priv->source, path); + node->expandable_set = 1; + node->num_visible_children = 0; + gnode = g_node_new (node); + g_hash_table_insert (etta->priv->nodes, path, gnode); + return gnode; +} + +static gint +insert_children (ETreeTableAdapter *etta, + GNode *gnode) +{ + ETreePath path, tmp; + gint count = 0; + gint pos = 0; + + path = ((node_t *) gnode->data)->path; + for (tmp = e_tree_model_node_get_first_child (etta->priv->source, path); + tmp; + tmp = e_tree_model_node_get_next (etta->priv->source, tmp), pos++) { + GNode *child = create_gnode (etta, tmp); + node_t *node = (node_t *) child->data; + if (node->expanded) + node->num_visible_children = insert_children (etta, child); + g_node_prepend (gnode, child); + count += node->num_visible_children + 1; + } + g_node_reverse_children (gnode); + return count; +} + +static void +generate_tree (ETreeTableAdapter *etta, + ETreePath path) +{ + GNode *gnode; + node_t *node; + gint size; + + e_table_model_pre_change (E_TABLE_MODEL (etta)); + + g_return_if_fail (e_tree_model_node_is_root (etta->priv->source, path)); + + if (etta->priv->root) + kill_gnode (etta->priv->root, etta); + resize_map (etta, 0); + + gnode = create_gnode (etta, path); + node = (node_t *) gnode->data; + node->expanded = TRUE; + node->num_visible_children = insert_children (etta, gnode); + if (etta->priv->sort_info && e_table_sort_info_sorting_get_count (etta->priv->sort_info) > 0) + resort_node (etta, gnode, TRUE); + + etta->priv->root = gnode; + size = etta->priv->root_visible ? node->num_visible_children + 1 : node->num_visible_children; + resize_map (etta, size); + fill_map (etta, 0, gnode); + e_table_model_changed (E_TABLE_MODEL (etta)); +} + +static void +insert_node (ETreeTableAdapter *etta, + ETreePath parent, + ETreePath path) +{ + GNode *gnode, *parent_gnode; + node_t *node, *parent_node; + gboolean expandable; + gint size, row; + + e_table_model_pre_change (E_TABLE_MODEL (etta)); + + if (get_node (etta, path)) { + e_table_model_no_change (E_TABLE_MODEL (etta)); + return; + } + + parent_gnode = lookup_gnode (etta, parent); + if (!parent_gnode) { + ETreePath grandparent = e_tree_model_node_get_parent (etta->priv->source, parent); + if (e_tree_model_node_is_root (etta->priv->source, parent)) + generate_tree (etta, parent); + else + insert_node (etta, grandparent, parent); + e_table_model_changed (E_TABLE_MODEL (etta)); + return; + } + + parent_node = (node_t *) parent_gnode->data; + + if (parent_gnode != etta->priv->root) { + expandable = e_tree_model_node_is_expandable (etta->priv->source, parent); + if (parent_node->expandable != expandable) { + e_table_model_pre_change (E_TABLE_MODEL (etta)); + parent_node->expandable = expandable; + parent_node->expandable_set = 1; + e_table_model_row_changed (E_TABLE_MODEL (etta), parent_node->index); + } + } + + if (!e_tree_table_adapter_node_is_expanded (etta, parent)) { + e_table_model_no_change (E_TABLE_MODEL (etta)); + return; + } + + gnode = create_gnode (etta, path); + node = (node_t *) gnode->data; + + if (node->expanded) + node->num_visible_children = insert_children (etta, gnode); + + g_node_append (parent_gnode, gnode); + update_child_counts (parent_gnode, node->num_visible_children + 1); + resort_node (etta, parent_gnode, FALSE); + resort_node (etta, gnode, TRUE); + + size = node->num_visible_children + 1; + resize_map (etta, etta->priv->n_map + size); + if (parent_gnode == etta->priv->root) + row = 0; + else { + gint new_size = parent_node->num_visible_children + 1; + gint old_size = new_size - size; + row = parent_node->index; + move_map_elements (etta, row + new_size, row + old_size, etta->priv->n_map - row - new_size); + } + fill_map (etta, row, parent_gnode); + e_table_model_rows_inserted (E_TABLE_MODEL (etta), get_row (etta, path), size); +} + +typedef struct { + GSList *paths; + gboolean expanded; +} check_expanded_closure; + +static gboolean +check_expanded (GNode *gnode, + gpointer data) +{ + check_expanded_closure *closure = (check_expanded_closure *) data; + node_t *node = (node_t *) gnode->data; + + if (node->expanded != closure->expanded) + closure->paths = g_slist_prepend (closure->paths, node->path); + + return FALSE; +} + +static void +update_node (ETreeTableAdapter *etta, + ETreePath path) +{ + check_expanded_closure closure; + ETreePath parent = e_tree_model_node_get_parent (etta->priv->source, path); + GNode *gnode = lookup_gnode (etta, path); + GSList *l; + + closure.expanded = e_tree_model_get_expanded_default (etta->priv->source); + closure.paths = NULL; + + if (gnode) + g_node_traverse (gnode, G_POST_ORDER, G_TRAVERSE_ALL, -1, check_expanded, &closure); + + if (e_tree_model_node_is_root (etta->priv->source, path)) + generate_tree (etta, path); + else { + delete_node (etta, parent, path); + insert_node (etta, parent, path); + } + + for (l = closure.paths; l; l = l->next) + if (lookup_gnode (etta, l->data)) + e_tree_table_adapter_node_set_expanded (etta, l->data, !closure.expanded); + + g_slist_free (closure.paths); +} + +static void +etta_finalize (GObject *object) +{ + ETreeTableAdapterPrivate *priv; + + priv = E_TREE_TABLE_ADAPTER_GET_PRIVATE (object); + + if (priv->resort_idle_id) { + g_source_remove (priv->resort_idle_id); + priv->resort_idle_id = 0; + } + + if (priv->root) { + kill_gnode (priv->root, E_TREE_TABLE_ADAPTER (object)); + priv->root = NULL; + } + + g_hash_table_destroy (priv->nodes); + + g_free (priv->map_table); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (etta_parent_class)->finalize (object); +} + +static void +etta_dispose (GObject *object) +{ + ETreeTableAdapterPrivate *priv; + + priv = E_TREE_TABLE_ADAPTER_GET_PRIVATE (object); + + if (priv->sort_info) { + g_signal_handler_disconnect ( + priv->sort_info, priv->sort_info_changed_id); + g_object_unref (priv->sort_info); + priv->sort_info = NULL; + } + + if (priv->header) { + g_object_unref (priv->header); + priv->header = NULL; + } + + if (priv->source) { + g_signal_handler_disconnect ( + priv->source, priv->pre_change_id); + g_signal_handler_disconnect ( + priv->source, priv->no_change_id); + g_signal_handler_disconnect ( + priv->source, priv->rebuilt_id); + g_signal_handler_disconnect ( + priv->source, priv->node_changed_id); + g_signal_handler_disconnect ( + priv->source, priv->node_data_changed_id); + g_signal_handler_disconnect ( + priv->source, priv->node_col_changed_id); + g_signal_handler_disconnect ( + priv->source, priv->node_inserted_id); + g_signal_handler_disconnect ( + priv->source, priv->node_removed_id); + g_signal_handler_disconnect ( + priv->source, priv->node_request_collapse_id); + + g_object_unref (priv->source); + priv->source = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (etta_parent_class)->dispose (object); +} + +static gint +etta_column_count (ETableModel *etm) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return e_tree_model_column_count (etta->priv->source); +} + +static gboolean +etta_has_save_id (ETableModel *etm) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return e_tree_model_has_save_id (etta->priv->source); +} + +static gchar * +etta_get_save_id (ETableModel *etm, + gint row) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return e_tree_model_get_save_id (etta->priv->source, get_path (etta, row)); +} + +static gboolean +etta_has_change_pending (ETableModel *etm) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return e_tree_model_has_change_pending (etta->priv->source); +} + +static gint +etta_row_count (ETableModel *etm) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return etta->priv->n_map; +} + +static gpointer +etta_value_at (ETableModel *etm, + gint col, + gint row) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + switch (col) { + case -1: + if (row == -1) + return NULL; + return get_path (etta, row); + case -2: + return etta->priv->source; + case -3: + return etta; + default: + return e_tree_model_value_at (etta->priv->source, get_path (etta, row), col); + } +} + +static void +etta_set_value_at (ETableModel *etm, + gint col, + gint row, + gconstpointer val) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + e_tree_model_set_value_at (etta->priv->source, get_path (etta, row), col, val); +} + +static gboolean +etta_is_cell_editable (ETableModel *etm, + gint col, + gint row) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return e_tree_model_node_is_editable (etta->priv->source, get_path (etta, row), col); +} + +static void +etta_append_row (ETableModel *etm, + ETableModel *source, + gint row) +{ +} + +static gpointer +etta_duplicate_value (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return e_tree_model_duplicate_value (etta->priv->source, col, value); +} + +static void +etta_free_value (ETableModel *etm, + gint col, + gpointer value) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + e_tree_model_free_value (etta->priv->source, col, value); +} + +static gpointer +etta_initialize_value (ETableModel *etm, + gint col) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return e_tree_model_initialize_value (etta->priv->source, col); +} + +static gboolean +etta_value_is_empty (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return e_tree_model_value_is_empty (etta->priv->source, col, value); +} + +static gchar * +etta_value_to_string (ETableModel *etm, + gint col, + gconstpointer value) +{ + ETreeTableAdapter *etta = (ETreeTableAdapter *) etm; + + return e_tree_model_value_to_string (etta->priv->source, col, value); +} + +static void +etta_class_init (ETreeTableAdapterClass *class) +{ + GObjectClass *object_class; + ETableModelClass *table_model_class; + + g_type_class_add_private (class, sizeof (ETreeTableAdapterPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = etta_dispose; + object_class->finalize = etta_finalize; + + table_model_class = E_TABLE_MODEL_CLASS (class); + table_model_class->column_count = etta_column_count; + table_model_class->row_count = etta_row_count; + table_model_class->append_row = etta_append_row; + + table_model_class->value_at = etta_value_at; + table_model_class->set_value_at = etta_set_value_at; + table_model_class->is_cell_editable = etta_is_cell_editable; + + table_model_class->has_save_id = etta_has_save_id; + table_model_class->get_save_id = etta_get_save_id; + + table_model_class->has_change_pending = etta_has_change_pending; + table_model_class->duplicate_value = etta_duplicate_value; + table_model_class->free_value = etta_free_value; + table_model_class->initialize_value = etta_initialize_value; + table_model_class->value_is_empty = etta_value_is_empty; + table_model_class->value_to_string = etta_value_to_string; + + class->sorting_changed = NULL; + + signals[SORTING_CHANGED] = g_signal_new ( + "sorting_changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeTableAdapterClass, sorting_changed), + NULL, NULL, + e_marshal_BOOLEAN__NONE, + G_TYPE_BOOLEAN, 0, + G_TYPE_NONE); +} + +static void +etta_init (ETreeTableAdapter *etta) +{ + etta->priv = E_TREE_TABLE_ADAPTER_GET_PRIVATE (etta); + + etta->priv->root_visible = TRUE; + etta->priv->remap_needed = TRUE; +} + +static void +etta_proxy_pre_change (ETreeModel *etm, + ETreeTableAdapter *etta) +{ + e_table_model_pre_change (E_TABLE_MODEL (etta)); +} + +static void +etta_proxy_no_change (ETreeModel *etm, + ETreeTableAdapter *etta) +{ + e_table_model_no_change (E_TABLE_MODEL (etta)); +} + +static void +etta_proxy_rebuilt (ETreeModel *etm, + ETreeTableAdapter *etta) +{ + if (!etta->priv->root) + return; + kill_gnode (etta->priv->root, etta); + etta->priv->root = NULL; + g_hash_table_destroy (etta->priv->nodes); + etta->priv->nodes = g_hash_table_new (NULL, NULL); +} + +static gboolean +resort_model (ETreeTableAdapter *etta) +{ + etta_sort_info_changed (NULL, etta); + etta->priv->resort_idle_id = 0; + return FALSE; +} + +static void +etta_proxy_node_changed (ETreeModel *etm, + ETreePath path, + ETreeTableAdapter *etta) +{ + update_node (etta, path); + e_table_model_changed (E_TABLE_MODEL (etta)); + + /* FIXME: Really it shouldnt be required. But a lot of thread + * which were supposed to be present in the list is way below + */ + if (!etta->priv->resort_idle_id) + etta->priv->resort_idle_id = g_idle_add ((GSourceFunc) resort_model, etta); +} + +static void +etta_proxy_node_data_changed (ETreeModel *etm, + ETreePath path, + ETreeTableAdapter *etta) +{ + gint row = get_row (etta, path); + + if (row == -1) { + e_table_model_no_change (E_TABLE_MODEL (etta)); + return; + } + + e_table_model_row_changed (E_TABLE_MODEL (etta), row); +} + +static void +etta_proxy_node_col_changed (ETreeModel *etm, + ETreePath path, + gint col, + ETreeTableAdapter *etta) +{ + gint row = get_row (etta, path); + + if (row == -1) { + e_table_model_no_change (E_TABLE_MODEL (etta)); + return; + } + + e_table_model_cell_changed (E_TABLE_MODEL (etta), col, row); +} + +static void +etta_proxy_node_inserted (ETreeModel *etm, + ETreePath parent, + ETreePath child, + ETreeTableAdapter *etta) +{ + if (e_tree_model_node_is_root (etm, child)) + generate_tree (etta, child); + else + insert_node (etta, parent, child); + + e_table_model_changed (E_TABLE_MODEL (etta)); +} + +static void +etta_proxy_node_removed (ETreeModel *etm, + ETreePath parent, + ETreePath child, + gint old_position, + ETreeTableAdapter *etta) +{ + delete_node (etta, parent, child); + e_table_model_changed (E_TABLE_MODEL (etta)); +} + +static void +etta_proxy_node_request_collapse (ETreeModel *etm, + ETreePath node, + ETreeTableAdapter *etta) +{ + e_tree_table_adapter_node_set_expanded (etta, node, FALSE); +} + +static void +etta_sort_info_changed (ETableSortInfo *sort_info, + ETreeTableAdapter *etta) +{ + if (!etta->priv->root) + return; + + /* the function is called also internally, with sort_info = NULL, + * thus skip those in signal emit */ + if (sort_info) { + gboolean handled = FALSE; + + g_signal_emit (etta, signals[SORTING_CHANGED], 0, &handled); + + if (handled) + return; + } + + e_table_model_pre_change (E_TABLE_MODEL (etta)); + resort_node (etta, etta->priv->root, TRUE); + fill_map (etta, 0, etta->priv->root); + e_table_model_changed (E_TABLE_MODEL (etta)); +} + +ETableModel * +e_tree_table_adapter_construct (ETreeTableAdapter *etta, + ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *header) +{ + ETreePath root; + + etta->priv->source = source; + g_object_ref (source); + + etta->priv->sort_info = sort_info; + if (sort_info) { + g_object_ref (sort_info); + etta->priv->sort_info_changed_id = g_signal_connect ( + sort_info, "sort_info_changed", + G_CALLBACK (etta_sort_info_changed), etta); + } + + etta->priv->header = header; + if (header) + g_object_ref (header); + + etta->priv->nodes = g_hash_table_new (NULL, NULL); + + root = e_tree_model_get_root (source); + + if (root) + generate_tree (etta, root); + + etta->priv->pre_change_id = g_signal_connect ( + source, "pre_change", + G_CALLBACK (etta_proxy_pre_change), etta); + etta->priv->no_change_id = g_signal_connect ( + source, "no_change", + G_CALLBACK (etta_proxy_no_change), etta); + etta->priv->rebuilt_id = g_signal_connect ( + source, "rebuilt", + G_CALLBACK (etta_proxy_rebuilt), etta); + etta->priv->node_changed_id = g_signal_connect ( + source, "node_changed", + G_CALLBACK (etta_proxy_node_changed), etta); + etta->priv->node_data_changed_id = g_signal_connect ( + source, "node_data_changed", + G_CALLBACK (etta_proxy_node_data_changed), etta); + etta->priv->node_col_changed_id = g_signal_connect ( + source, "node_col_changed", + G_CALLBACK (etta_proxy_node_col_changed), etta); + etta->priv->node_inserted_id = g_signal_connect ( + source, "node_inserted", + G_CALLBACK (etta_proxy_node_inserted), etta); + etta->priv->node_removed_id = g_signal_connect ( + source, "node_removed", + G_CALLBACK (etta_proxy_node_removed), etta); + etta->priv->node_request_collapse_id = g_signal_connect ( + source, "node_request_collapse", + G_CALLBACK (etta_proxy_node_request_collapse), etta); + + return E_TABLE_MODEL (etta); +} + +ETableModel * +e_tree_table_adapter_new (ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *header) +{ + ETreeTableAdapter *etta = g_object_new (E_TYPE_TREE_TABLE_ADAPTER, NULL); + + e_tree_table_adapter_construct (etta, source, sort_info, header); + + return (ETableModel *) etta; +} + +typedef struct { + xmlNode *root; + gboolean expanded_default; + ETreeModel *model; +} TreeAndRoot; + +static void +save_expanded_state_func (gpointer keyp, + gpointer value, + gpointer data) +{ + ETreePath path = keyp; + node_t *node = ((GNode *) value)->data; + TreeAndRoot *tar = data; + xmlNode *xmlnode; + + if (node->expanded != tar->expanded_default) { + gchar *save_id = e_tree_model_get_save_id (tar->model, path); + xmlnode = xmlNewChild (tar->root, NULL, (const guchar *)"node", NULL); + e_xml_set_string_prop_by_name (xmlnode, (const guchar *)"id", save_id); + g_free (save_id); + } +} + +xmlDoc * +e_tree_table_adapter_save_expanded_state_xml (ETreeTableAdapter *etta) +{ + TreeAndRoot tar; + xmlDocPtr doc; + xmlNode *root; + + g_return_val_if_fail (etta != NULL, NULL); + + doc = xmlNewDoc ((const guchar *)"1.0"); + root = xmlNewDocNode (doc, NULL, (const guchar *)"expanded_state", NULL); + xmlDocSetRootElement (doc, root); + + tar.model = etta->priv->source; + tar.root = root; + tar.expanded_default = e_tree_model_get_expanded_default (etta->priv->source); + + e_xml_set_integer_prop_by_name (root, (const guchar *)"vers", 2); + e_xml_set_bool_prop_by_name (root, (const guchar *)"default", tar.expanded_default); + + g_hash_table_foreach (etta->priv->nodes, save_expanded_state_func, &tar); + + return doc; +} + +void +e_tree_table_adapter_save_expanded_state (ETreeTableAdapter *etta, + const gchar *filename) +{ + xmlDoc *doc; + + g_return_if_fail (etta != NULL); + + doc = e_tree_table_adapter_save_expanded_state_xml (etta); + if (doc) { + e_xml_save_file (filename, doc); + xmlFreeDoc (doc); + } +} + +static xmlDoc * +open_file (ETreeTableAdapter *etta, + const gchar *filename) +{ + xmlDoc *doc; + xmlNode *root; + gint vers; + gboolean model_default, saved_default; + + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) + return NULL; + +#ifdef G_OS_WIN32 + { + gchar *locale_filename = g_win32_locale_filename_from_utf8 (filename); + doc = xmlParseFile (locale_filename); + g_free (locale_filename); + } +#else + doc = xmlParseFile (filename); +#endif + + if (!doc) + return NULL; + + root = xmlDocGetRootElement (doc); + if (root == NULL || strcmp ((gchar *) root->name, "expanded_state")) { + xmlFreeDoc (doc); + return NULL; + } + + vers = e_xml_get_integer_prop_by_name_with_default (root, (const guchar *)"vers", 0); + if (vers > 2) { + xmlFreeDoc (doc); + return NULL; + } + model_default = e_tree_model_get_expanded_default (etta->priv->source); + saved_default = e_xml_get_bool_prop_by_name_with_default (root, (const guchar *)"default", !model_default); + if (saved_default != model_default) { + xmlFreeDoc (doc); + return NULL; + } + + return doc; +} + +/* state: <0 ... collapse; 0 ... use default; >0 ... expand */ +void +e_tree_table_adapter_force_expanded_state (ETreeTableAdapter *etta, + gint state) +{ + g_return_if_fail (etta != NULL); + + etta->priv->force_expanded_state = state; +} + +void +e_tree_table_adapter_load_expanded_state_xml (ETreeTableAdapter *etta, + xmlDoc *doc) +{ + xmlNode *root, *child; + gboolean model_default; + gboolean file_default = FALSE; + + g_return_if_fail (etta != NULL); + g_return_if_fail (doc != NULL); + + root = xmlDocGetRootElement (doc); + + e_table_model_pre_change (E_TABLE_MODEL (etta)); + + model_default = e_tree_model_get_expanded_default (etta->priv->source); + + if (!strcmp ((gchar *) root->name, "expanded_state")) { + gchar *state; + + state = e_xml_get_string_prop_by_name_with_default (root, (const guchar *)"default", ""); + + if (state[0] == 't') + file_default = TRUE; + else + file_default = FALSE; /* Even unspecified we'll consider as false */ + + g_free (state); + } + + /* Incase the default is changed, lets forget the changes and stick to default */ + + if (file_default != model_default) { + xmlFreeDoc (doc); + return; + } + + for (child = root->xmlChildrenNode; child; child = child->next) { + gchar *id; + ETreePath path; + + if (strcmp ((gchar *) child->name, "node")) { + d (g_warning ("unknown node '%s' in %s", child->name, filename)); + continue; + } + + id = e_xml_get_string_prop_by_name_with_default (child, (const guchar *)"id", ""); + + if (!strcmp (id, "")) { + g_free (id); + continue; + } + + path = e_tree_model_get_node_by_id (etta->priv->source, id); + if (path) + e_tree_table_adapter_node_set_expanded (etta, path, !model_default); + + g_free (id); + } + + e_table_model_changed (E_TABLE_MODEL (etta)); +} + +void +e_tree_table_adapter_load_expanded_state (ETreeTableAdapter *etta, + const gchar *filename) +{ + xmlDoc *doc; + + g_return_if_fail (etta != NULL); + + doc = open_file (etta, filename); + if (!doc) + return; + + e_tree_table_adapter_load_expanded_state_xml (etta, doc); + + xmlFreeDoc (doc); +} + +void +e_tree_table_adapter_root_node_set_visible (ETreeTableAdapter *etta, + gboolean visible) +{ + gint size; + + g_return_if_fail (etta != NULL); + + if (etta->priv->root_visible == visible) + return; + + e_table_model_pre_change (E_TABLE_MODEL (etta)); + + etta->priv->root_visible = visible; + if (!visible) { + ETreePath root = e_tree_model_get_root (etta->priv->source); + if (root) + e_tree_table_adapter_node_set_expanded (etta, root, TRUE); + } + size = (visible ? 1 : 0) + (etta->priv->root ? ((node_t *) etta->priv->root->data)->num_visible_children : 0); + resize_map (etta, size); + if (etta->priv->root) + fill_map (etta, 0, etta->priv->root); + e_table_model_changed (E_TABLE_MODEL (etta)); +} + +void +e_tree_table_adapter_node_set_expanded (ETreeTableAdapter *etta, + ETreePath path, + gboolean expanded) +{ + GNode *gnode = lookup_gnode (etta, path); + node_t *node; + gint row; + + if (!expanded && (!gnode || (e_tree_model_node_is_root (etta->priv->source, path) && !etta->priv->root_visible))) + return; + + if (!gnode && expanded) { + ETreePath parent = e_tree_model_node_get_parent (etta->priv->source, path); + g_return_if_fail (parent != NULL); + e_tree_table_adapter_node_set_expanded (etta, parent, expanded); + gnode = lookup_gnode (etta, path); + } + g_return_if_fail (gnode != NULL); + + node = (node_t *) gnode->data; + + if (expanded == node->expanded) + return; + + node->expanded = expanded; + + row = get_row (etta, path); + if (row == -1) + return; + + e_table_model_pre_change (E_TABLE_MODEL (etta)); + e_table_model_pre_change (E_TABLE_MODEL (etta)); + e_table_model_row_changed (E_TABLE_MODEL (etta), row); + + if (expanded) { + gint num_children = insert_children (etta, gnode); + update_child_counts (gnode, num_children); + if (etta->priv->sort_info && e_table_sort_info_sorting_get_count (etta->priv->sort_info) > 0) + resort_node (etta, gnode, TRUE); + resize_map (etta, etta->priv->n_map + num_children); + move_map_elements (etta, row + 1 + num_children, row + 1, etta->priv->n_map - row - 1 - num_children); + fill_map (etta, row, gnode); + if (num_children != 0) { + e_table_model_rows_inserted (E_TABLE_MODEL (etta), row + 1, num_children); + } else + e_table_model_no_change (E_TABLE_MODEL (etta)); + } else { + gint num_children = delete_children (etta, gnode); + if (num_children == 0) { + e_table_model_no_change (E_TABLE_MODEL (etta)); + return; + } + move_map_elements (etta, row + 1, row + 1 + num_children, etta->priv->n_map - row - 1 - num_children); + update_child_counts (gnode, - num_children); + resize_map (etta, etta->priv->n_map - num_children); + e_table_model_rows_deleted (E_TABLE_MODEL (etta), row + 1, num_children); + } +} + +void +e_tree_table_adapter_node_set_expanded_recurse (ETreeTableAdapter *etta, + ETreePath path, + gboolean expanded) +{ + ETreePath children; + + e_tree_table_adapter_node_set_expanded (etta, path, expanded); + + for (children = e_tree_model_node_get_first_child (etta->priv->source, path); + children; + children = e_tree_model_node_get_next (etta->priv->source, children)) { + e_tree_table_adapter_node_set_expanded_recurse (etta, children, expanded); + } +} + +ETreePath +e_tree_table_adapter_node_at_row (ETreeTableAdapter *etta, + gint row) +{ + return get_path (etta, row); +} + +gint +e_tree_table_adapter_row_of_node (ETreeTableAdapter *etta, + ETreePath path) +{ + return get_row (etta, path); +} + +gboolean +e_tree_table_adapter_root_node_is_visible (ETreeTableAdapter *etta) +{ + return etta->priv->root_visible; +} + +void +e_tree_table_adapter_show_node (ETreeTableAdapter *etta, + ETreePath path) +{ + ETreePath parent; + + parent = e_tree_model_node_get_parent (etta->priv->source, path); + + while (parent) { + e_tree_table_adapter_node_set_expanded (etta, parent, TRUE); + parent = e_tree_model_node_get_parent (etta->priv->source, parent); + } +} + +gboolean +e_tree_table_adapter_node_is_expanded (ETreeTableAdapter *etta, + ETreePath path) +{ + node_t *node = get_node (etta, path); + if (!e_tree_model_node_is_expandable (etta->priv->source, path) || !node) + return FALSE; + + return node->expanded; +} + +void +e_tree_table_adapter_set_sort_info (ETreeTableAdapter *etta, + ETableSortInfo *sort_info) +{ + if (etta->priv->sort_info) { + g_signal_handler_disconnect ( + etta->priv->sort_info, + etta->priv->sort_info_changed_id); + g_object_unref (etta->priv->sort_info); + } + + etta->priv->sort_info = sort_info; + if (sort_info) { + g_object_ref (sort_info); + etta->priv->sort_info_changed_id = g_signal_connect ( + sort_info, "sort_info_changed", + G_CALLBACK (etta_sort_info_changed), etta); + } + + if (!etta->priv->root) + return; + + e_table_model_pre_change (E_TABLE_MODEL (etta)); + resort_node (etta, etta->priv->root, TRUE); + fill_map (etta, 0, etta->priv->root); + e_table_model_changed (E_TABLE_MODEL (etta)); +} + +ETableSortInfo * +e_tree_table_adapter_get_sort_info (ETreeTableAdapter *etta) +{ + g_return_val_if_fail (etta != NULL, NULL); + + return etta->priv->sort_info; +} + +ETableHeader * +e_tree_table_adapter_get_header (ETreeTableAdapter *etta) +{ + g_return_val_if_fail (etta != NULL, NULL); + + return etta->priv->header; +} + +ETreePath +e_tree_table_adapter_node_get_next (ETreeTableAdapter *etta, + ETreePath path) +{ + GNode *node = lookup_gnode (etta, path); + + if (node && node->next) + return ((node_t *) node->next->data)->path; + + return NULL; +} diff --git a/e-util/e-tree-table-adapter.h b/e-util/e-tree-table-adapter.h new file mode 100644 index 0000000000..17f3304aa4 --- /dev/null +++ b/e-util/e-tree-table-adapter.h @@ -0,0 +1,138 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * Chris Toshok <toshok@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TREE_TABLE_ADAPTER_H_ +#define _E_TREE_TABLE_ADAPTER_H_ + +#include <libxml/tree.h> + +#include <e-util/e-table-header.h> +#include <e-util/e-table-model.h> +#include <e-util/e-table-sort-info.h> +#include <e-util/e-tree-model.h> + +/* Standard GObject macros */ +#define E_TYPE_TREE_TABLE_ADAPTER \ + (e_tree_table_adapter_get_type ()) +#define E_TREE_TABLE_ADAPTER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TREE_TABLE_ADAPTER, ETreeTableAdapter)) +#define E_TREE_TABLE_ADAPTER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TREE_TABLE_ADAPTER, ETreeTableAdapterClass)) +#define E_IS_TREE_TABLE_ADAPTER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TREE_TABLE_ADAPTER)) +#define E_IS_TREE_TABLE_ADAPTER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TREE_TABLE_ADAPTER)) +#define E_TREE_TABLE_ADAPTER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TREE_TABLE_ADAPTER, ETreeTableAdapterClass)) + +G_BEGIN_DECLS + +typedef struct _ETreeTableAdapter ETreeTableAdapter; +typedef struct _ETreeTableAdapterClass ETreeTableAdapterClass; +typedef struct _ETreeTableAdapterPrivate ETreeTableAdapterPrivate; + +struct _ETreeTableAdapter { + ETableModel parent; + ETreeTableAdapterPrivate *priv; +}; + +struct _ETreeTableAdapterClass { + ETableModelClass parent_class; + + /* Signals */ + gboolean (*sorting_changed) (ETreeTableAdapter *etta); +}; + +GType e_tree_table_adapter_get_type (void) G_GNUC_CONST; +ETableModel * e_tree_table_adapter_new (ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *header); +ETableModel * e_tree_table_adapter_construct (ETreeTableAdapter *ets, + ETreeModel *source, + ETableSortInfo *sort_info, + ETableHeader *header); + +ETreePath e_tree_table_adapter_node_get_next + (ETreeTableAdapter *etta, + ETreePath path); +gboolean e_tree_table_adapter_node_is_expanded + (ETreeTableAdapter *etta, + ETreePath path); +void e_tree_table_adapter_node_set_expanded + (ETreeTableAdapter *etta, + ETreePath path, + gboolean expanded); +void e_tree_table_adapter_node_set_expanded_recurse + (ETreeTableAdapter *etta, + ETreePath path, + gboolean expanded); +void e_tree_table_adapter_force_expanded_state + (ETreeTableAdapter *etta, + gint state); +void e_tree_table_adapter_root_node_set_visible + (ETreeTableAdapter *etta, + gboolean visible); +ETreePath e_tree_table_adapter_node_at_row + (ETreeTableAdapter *etta, + gint row); +gint e_tree_table_adapter_row_of_node + (ETreeTableAdapter *etta, + ETreePath path); +gboolean e_tree_table_adapter_root_node_is_visible + (ETreeTableAdapter *etta); + +void e_tree_table_adapter_show_node (ETreeTableAdapter *etta, + ETreePath path); + +void e_tree_table_adapter_save_expanded_state + (ETreeTableAdapter *etta, + const gchar *filename); +void e_tree_table_adapter_load_expanded_state + (ETreeTableAdapter *etta, + const gchar *filename); + +xmlDoc * e_tree_table_adapter_save_expanded_state_xml + (ETreeTableAdapter *etta); +void e_tree_table_adapter_load_expanded_state_xml + (ETreeTableAdapter *etta, + xmlDoc *doc); + +void e_tree_table_adapter_set_sort_info + (ETreeTableAdapter *etta, + ETableSortInfo *sort_info); +ETableSortInfo *e_tree_table_adapter_get_sort_info + (ETreeTableAdapter *etta); +ETableHeader * e_tree_table_adapter_get_header (ETreeTableAdapter *etta); + +G_END_DECLS + +#endif /* _E_TREE_TABLE_ADAPTER_H_ */ diff --git a/e-util/e-tree.c b/e-util/e-tree.c new file mode 100644 index 0000000000..ee451cd28a --- /dev/null +++ b/e-util/e-tree.c @@ -0,0 +1,3956 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include <libgnomecanvas/libgnomecanvas.h> + +#include "e-canvas-background.h" +#include "e-canvas-utils.h" +#include "e-canvas.h" +#include "e-table-column-specification.h" +#include "e-table-header-item.h" +#include "e-table-header.h" +#include "e-table-item.h" +#include "e-table-sort-info.h" +#include "e-table-utils.h" +#include "e-text.h" +#include "e-tree-table-adapter.h" +#include "e-tree.h" +#include "gal-a11y-e-tree.h" + +#ifdef E_TREE_USE_TREE_SELECTION +#include "e-tree-selection-model.h" +#else +#include "e-table-selection-model.h" +#endif + +#define COLUMN_HEADER_HEIGHT 16 + +#define d(x) + +#define E_TREE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_TREE, ETreePrivate)) + +enum { + CURSOR_CHANGE, + CURSOR_ACTIVATED, + SELECTION_CHANGE, + DOUBLE_CLICK, + RIGHT_CLICK, + CLICK, + KEY_PRESS, + START_DRAG, + STATE_CHANGE, + WHITE_SPACE_EVENT, + + CUT_CLIPBOARD, + COPY_CLIPBOARD, + PASTE_CLIPBOARD, + SELECT_ALL, + + TREE_DRAG_BEGIN, + TREE_DRAG_END, + TREE_DRAG_DATA_GET, + TREE_DRAG_DATA_DELETE, + + TREE_DRAG_LEAVE, + TREE_DRAG_MOTION, + TREE_DRAG_DROP, + TREE_DRAG_DATA_RECEIVED, + + LAST_SIGNAL +}; + +enum { + PROP_0, + PROP_LENGTH_THRESHOLD, + PROP_HORIZONTAL_DRAW_GRID, + PROP_VERTICAL_DRAW_GRID, + PROP_DRAW_FOCUS, + PROP_ETTA, + PROP_UNIFORM_ROW_HEIGHT, + PROP_ALWAYS_SEARCH, + PROP_HADJUSTMENT, + PROP_VADJUSTMENT, + PROP_HSCROLL_POLICY, + PROP_VSCROLL_POLICY +}; + +enum { + ET_SCROLL_UP = 1 << 0, + ET_SCROLL_DOWN = 1 << 1, + ET_SCROLL_LEFT = 1 << 2, + ET_SCROLL_RIGHT = 1 << 3 +}; + +struct _ETreePrivate { + ETreeModel *model; + ETreeTableAdapter *etta; + + ETableHeader *full_header, *header; + + guint structure_change_id, expansion_change_id; + + ETableSortInfo *sort_info; + ESorter *sorter; + + guint sort_info_change_id, group_info_change_id; + + ESelectionModel *selection; + ETableSpecification *spec; + + ETableSearch *search; + + ETableCol *current_search_col; + + guint search_search_id; + guint search_accept_id; + + gint reflow_idle_id; + gint scroll_idle_id; + gint hover_idle_id; + + gboolean show_cursor_after_reflow; + + gint table_model_change_id; + gint table_row_change_id; + gint table_cell_change_id; + gint table_rows_delete_id; + + GnomeCanvasItem *info_text; + guint info_text_resize_id; + + GnomeCanvas *header_canvas, *table_canvas; + + GnomeCanvasItem *header_item, *root; + + GnomeCanvasItem *white_item; + GnomeCanvasItem *item; + + gint length_threshold; + + /* + * Configuration settings + */ + guint alternating_row_colors : 1; + guint horizontal_draw_grid : 1; + guint vertical_draw_grid : 1; + guint draw_focus : 1; + guint row_selection_active : 1; + + guint horizontal_scrolling : 1; + + guint scroll_direction : 4; + + guint do_drag : 1; + + guint uniform_row_height : 1; + + guint search_col_set : 1; + guint always_search : 1; + + ECursorMode cursor_mode; + + gint drop_row; + ETreePath drop_path; + gint drop_col; + + GnomeCanvasItem *drop_highlight; + gint last_drop_x; + gint last_drop_y; + gint last_drop_time; + GdkDragContext *last_drop_context; + + gint hover_x; + gint hover_y; + + gint drag_row; + ETreePath drag_path; + gint drag_col; + ETreeDragSourceSite *site; + + GList *expanded_list; + + gboolean state_changed; + guint state_change_freeze; + + gboolean is_dragging; +}; + +static guint et_signals[LAST_SIGNAL] = { 0, }; + +static void et_grab_focus (GtkWidget *widget); + +static void et_drag_begin (GtkWidget *widget, + GdkDragContext *context, + ETree *et); +static void et_drag_end (GtkWidget *widget, + GdkDragContext *context, + ETree *et); +static void et_drag_data_get (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time, + ETree *et); +static void et_drag_data_delete (GtkWidget *widget, + GdkDragContext *context, + ETree *et); + +static void et_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time, + ETree *et); +static gboolean et_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETree *et); +static gboolean et_drag_drop (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETree *et); +static void et_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time, + ETree *et); + +static void scroll_off (ETree *et); +static void scroll_on (ETree *et, guint scroll_direction); +static void hover_off (ETree *et); +static void hover_on (ETree *et, gint x, gint y); +static void context_destroyed (gpointer data, GObject *ctx); + +G_DEFINE_TYPE_WITH_CODE (ETree, e_tree, GTK_TYPE_TABLE, + G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL)) + +static void +et_disconnect_from_etta (ETree *et) +{ + if (et->priv->table_model_change_id != 0) + g_signal_handler_disconnect ( + et->priv->etta, + et->priv->table_model_change_id); + if (et->priv->table_row_change_id != 0) + g_signal_handler_disconnect ( + et->priv->etta, + et->priv->table_row_change_id); + if (et->priv->table_cell_change_id != 0) + g_signal_handler_disconnect ( + et->priv->etta, + et->priv->table_cell_change_id); + if (et->priv->table_rows_delete_id != 0) + g_signal_handler_disconnect ( + et->priv->etta, + et->priv->table_rows_delete_id); + + et->priv->table_model_change_id = 0; + et->priv->table_row_change_id = 0; + et->priv->table_cell_change_id = 0; + et->priv->table_rows_delete_id = 0; +} + +static void +clear_current_search_col (ETree *et) +{ + et->priv->search_col_set = FALSE; +} + +static ETableCol * +current_search_col (ETree *et) +{ + if (!et->priv->search_col_set) { + et->priv->current_search_col = + e_table_util_calculate_current_search_col ( + et->priv->header, + et->priv->full_header, + et->priv->sort_info, + et->priv->always_search); + et->priv->search_col_set = TRUE; + } + + return et->priv->current_search_col; +} + +static void +e_tree_state_change (ETree *et) +{ + if (et->priv->state_change_freeze) + et->priv->state_changed = TRUE; + else + g_signal_emit (et, et_signals[STATE_CHANGE], 0); +} + +static void +change_trigger (GObject *object, + ETree *et) +{ + e_tree_state_change (et); +} + +static void +search_col_change_trigger (GObject *object, + ETree *et) +{ + clear_current_search_col (et); + e_tree_state_change (et); +} + +static void +disconnect_header (ETree *e_tree) +{ + if (e_tree->priv->header == NULL) + return; + + if (e_tree->priv->structure_change_id) + g_signal_handler_disconnect ( + e_tree->priv->header, + e_tree->priv->structure_change_id); + if (e_tree->priv->expansion_change_id) + g_signal_handler_disconnect ( + e_tree->priv->header, + e_tree->priv->expansion_change_id); + if (e_tree->priv->sort_info) { + if (e_tree->priv->sort_info_change_id) + g_signal_handler_disconnect ( + e_tree->priv->sort_info, + e_tree->priv->sort_info_change_id); + if (e_tree->priv->group_info_change_id) + g_signal_handler_disconnect ( + e_tree->priv->sort_info, + e_tree->priv->group_info_change_id); + + g_object_unref (e_tree->priv->sort_info); + } + g_object_unref (e_tree->priv->header); + e_tree->priv->header = NULL; + e_tree->priv->sort_info = NULL; +} + +static void +connect_header (ETree *e_tree, + ETableState *state) +{ + GValue *val = g_new0 (GValue, 1); + + if (e_tree->priv->header != NULL) + disconnect_header (e_tree); + + e_tree->priv->header = e_table_state_to_header ( + GTK_WIDGET (e_tree), e_tree->priv->full_header, state); + + e_tree->priv->structure_change_id = g_signal_connect ( + e_tree->priv->header, "structure_change", + G_CALLBACK (search_col_change_trigger), e_tree); + + e_tree->priv->expansion_change_id = g_signal_connect ( + e_tree->priv->header, "expansion_change", + G_CALLBACK (change_trigger), e_tree); + + if (state->sort_info) { + e_tree->priv->sort_info = e_table_sort_info_duplicate (state->sort_info); + e_table_sort_info_set_can_group (e_tree->priv->sort_info, FALSE); + e_tree->priv->sort_info_change_id = g_signal_connect ( + e_tree->priv->sort_info, "sort_info_changed", + G_CALLBACK (search_col_change_trigger), e_tree); + + e_tree->priv->group_info_change_id = g_signal_connect ( + e_tree->priv->sort_info, "group_info_changed", + G_CALLBACK (search_col_change_trigger), e_tree); + } else + e_tree->priv->sort_info = NULL; + + g_value_init (val, G_TYPE_OBJECT); + g_value_set_object (val, e_tree->priv->sort_info); + g_object_set_property (G_OBJECT (e_tree->priv->header), "sort_info", val); + g_free (val); +} + +static void +et_dispose (GObject *object) +{ + ETreePrivate *priv; + + priv = E_TREE_GET_PRIVATE (object); + + if (priv->search != NULL) { + g_signal_handler_disconnect ( + priv->search, priv->search_search_id); + g_signal_handler_disconnect ( + priv->search, priv->search_accept_id); + g_object_unref (priv->search); + priv->search = NULL; + } + + if (priv->reflow_idle_id > 0) { + g_source_remove (priv->reflow_idle_id); + priv->reflow_idle_id = 0; + } + + scroll_off (E_TREE (object)); + hover_off (E_TREE (object)); + g_list_foreach ( + priv->expanded_list, + (GFunc) g_free, NULL); + g_list_free (priv->expanded_list); + priv->expanded_list = NULL; + + et_disconnect_from_etta (E_TREE (object)); + + if (priv->etta != NULL) { + g_object_unref (priv->etta); + priv->etta = NULL; + } + + if (priv->model != NULL) { + g_object_unref (priv->model); + priv->model = NULL; + } + + if (priv->full_header != NULL) { + g_object_unref (priv->full_header); + priv->full_header = NULL; + } + + disconnect_header (E_TREE (object)); + + if (priv->selection != NULL) { + g_object_unref (priv->selection); + priv->selection = NULL; + } + + if (priv->spec != NULL) { + g_object_unref (priv->spec); + priv->spec = NULL; + } + + if (priv->sorter != NULL) { + g_object_unref (priv->sorter); + priv->sorter = NULL; + } + + if (priv->header_canvas != NULL) { + gtk_widget_destroy (GTK_WIDGET (priv->header_canvas)); + priv->header_canvas = NULL; + } + + if (priv->site) + e_tree_drag_source_unset (E_TREE (object)); + + if (priv->last_drop_context != NULL) { + g_object_weak_unref ( + G_OBJECT (priv->last_drop_context), + context_destroyed, object); + priv->last_drop_context = NULL; + } + + if (priv->info_text != NULL) { + g_object_run_dispose (G_OBJECT (priv->info_text)); + priv->info_text = NULL; + } + priv->info_text_resize_id = 0; + + if (priv->table_canvas != NULL) { + gtk_widget_destroy (GTK_WIDGET (priv->table_canvas)); + priv->table_canvas = NULL; + } + + /* do not unref it, it was owned by priv->table_canvas */ + priv->item = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_tree_parent_class)->dispose (object); +} + +static void +et_unrealize (GtkWidget *widget) +{ + scroll_off (E_TREE (widget)); + hover_off (E_TREE (widget)); + + if (GTK_WIDGET_CLASS (e_tree_parent_class)->unrealize) + GTK_WIDGET_CLASS (e_tree_parent_class)->unrealize (widget); +} + +typedef struct { + ETree *et; + gchar *string; +} SearchSearchStruct; + +static gboolean +search_search_callback (ETreeModel *model, + ETreePath path, + gpointer data) +{ + SearchSearchStruct *cb_data = data; + gconstpointer value; + ETableCol *col = current_search_col (cb_data->et); + + value = e_tree_model_value_at ( + model, path, cb_data->et->priv->current_search_col->col_idx); + + return col->search (value, cb_data->string); +} + +static gboolean +et_search_search (ETableSearch *search, + gchar *string, + ETableSearchFlags flags, + ETree *et) +{ + ETreePath cursor; + ETreePath found; + SearchSearchStruct cb_data; + ETableCol *col = current_search_col (et); + + if (col == NULL) + return FALSE; + + cb_data.et = et; + cb_data.string = string; + + cursor = e_tree_get_cursor (et); + + if (cursor && (flags & E_TABLE_SEARCH_FLAGS_CHECK_CURSOR_FIRST)) { + gconstpointer value; + + value = e_tree_model_value_at (et->priv->model, cursor, col->col_idx); + + if (col->search (value, string)) { + return TRUE; + } + } + + found = e_tree_model_node_find ( + et->priv->model, cursor, NULL, + E_TREE_FIND_NEXT_FORWARD, + search_search_callback, &cb_data); + if (found == NULL) + found = e_tree_model_node_find ( + et->priv->model, NULL, cursor, + E_TREE_FIND_NEXT_FORWARD, + search_search_callback, &cb_data); + + if (found && found != cursor) { + gint model_row; + + e_tree_table_adapter_show_node (et->priv->etta, found); + model_row = e_tree_table_adapter_row_of_node (et->priv->etta, found); + + e_selection_model_select_as_key_press ( + E_SELECTION_MODEL (et->priv->selection), + model_row, col->col_idx, GDK_CONTROL_MASK); + return TRUE; + } else if (cursor && !(flags & E_TABLE_SEARCH_FLAGS_CHECK_CURSOR_FIRST)) { + gconstpointer value; + + value = e_tree_model_value_at (et->priv->model, cursor, col->col_idx); + + return col->search (value, string); + } else + return FALSE; +} + +static void +et_search_accept (ETableSearch *search, + ETree *et) +{ + ETableCol *col = current_search_col (et); + gint cursor; + + if (col == NULL) + return; + + g_object_get (et->priv->selection, "cursor_row", &cursor, NULL); + + e_selection_model_select_as_key_press ( + E_SELECTION_MODEL (et->priv->selection), + cursor, col->col_idx, 0); +} + +static void +e_tree_init (ETree *e_tree) +{ + gtk_widget_set_can_focus (GTK_WIDGET (e_tree), TRUE); + + gtk_table_set_homogeneous (GTK_TABLE (e_tree), FALSE); + + e_tree->priv = E_TREE_GET_PRIVATE (e_tree); + + e_tree->priv->alternating_row_colors = 1; + e_tree->priv->horizontal_draw_grid = 1; + e_tree->priv->vertical_draw_grid = 1; + e_tree->priv->draw_focus = 1; + e_tree->priv->cursor_mode = E_CURSOR_SIMPLE; + e_tree->priv->length_threshold = 200; + + e_tree->priv->drop_row = -1; + e_tree->priv->drop_col = -1; + + e_tree->priv->drag_row = -1; + e_tree->priv->drag_col = -1; + +#ifdef E_TREE_USE_TREE_SELECTION + e_tree->priv->selection = + E_SELECTION_MODEL (e_tree_selection_model_new ()); +#else + e_tree->priv->selection = + E_SELECTION_MODEL (e_table_selection_model_new ()); +#endif + + e_tree->priv->search = e_table_search_new (); + + e_tree->priv->search_search_id = g_signal_connect ( + e_tree->priv->search, "search", + G_CALLBACK (et_search_search), e_tree); + + e_tree->priv->search_accept_id = g_signal_connect ( + e_tree->priv->search, "accept", + G_CALLBACK (et_search_accept), e_tree); + + e_tree->priv->always_search = g_getenv ("GAL_ALWAYS_SEARCH") ? TRUE : FALSE; + + e_tree->priv->state_changed = FALSE; + e_tree->priv->state_change_freeze = 0; + + e_tree->priv->is_dragging = FALSE; +} + +/* Grab_focus handler for the ETree */ +static void +et_grab_focus (GtkWidget *widget) +{ + ETree *e_tree; + + e_tree = E_TREE (widget); + + gtk_widget_grab_focus (GTK_WIDGET (e_tree->priv->table_canvas)); +} + +/* Focus handler for the ETree */ +static gint +et_focus (GtkWidget *container, + GtkDirectionType direction) +{ + ETree *e_tree; + + e_tree = E_TREE (container); + + if (gtk_container_get_focus_child (GTK_CONTAINER (container))) { + gtk_container_set_focus_child (GTK_CONTAINER (container), NULL); + return FALSE; + } + + return gtk_widget_child_focus ( + GTK_WIDGET (e_tree->priv->table_canvas), direction); +} + +static void +set_header_canvas_width (ETree *e_tree) +{ + gdouble oldwidth, oldheight, width; + + if (!(e_tree->priv->header_item && + e_tree->priv->header_canvas && e_tree->priv->table_canvas)) + return; + + gnome_canvas_get_scroll_region ( + GNOME_CANVAS (e_tree->priv->table_canvas), + NULL, NULL, &width, NULL); + gnome_canvas_get_scroll_region ( + GNOME_CANVAS (e_tree->priv->header_canvas), + NULL, NULL, &oldwidth, &oldheight); + + if (oldwidth != width || + oldheight != E_TABLE_HEADER_ITEM (e_tree->priv->header_item)->height - 1) + gnome_canvas_set_scroll_region ( + GNOME_CANVAS (e_tree->priv->header_canvas), + 0, 0, width, /* COLUMN_HEADER_HEIGHT - 1 */ + E_TABLE_HEADER_ITEM (e_tree->priv->header_item)->height - 1); + +} + +static void +header_canvas_size_allocate (GtkWidget *widget, + GtkAllocation *alloc, + ETree *e_tree) +{ + GtkAllocation allocation; + + set_header_canvas_width (e_tree); + + widget = GTK_WIDGET (e_tree->priv->header_canvas); + gtk_widget_get_allocation (widget, &allocation); + + /* When the header item is created ->height == 0, + * as the font is only created when everything is realized. + * So we set the usize here as well, so that the size of the + * header is correct */ + if (allocation.height != E_TABLE_HEADER_ITEM (e_tree->priv->header_item)->height) + gtk_widget_set_size_request ( + widget, -1, + E_TABLE_HEADER_ITEM (e_tree->priv->header_item)->height); +} + +static void +e_tree_setup_header (ETree *e_tree) +{ + GtkWidget *widget; + gchar *pointer; + + widget = e_canvas_new (); + gtk_widget_set_can_focus (widget, FALSE); + e_tree->priv->header_canvas = GNOME_CANVAS (widget); + gtk_widget_show (widget); + + pointer = g_strdup_printf ("%p", (gpointer) e_tree); + + e_tree->priv->header_item = gnome_canvas_item_new ( + gnome_canvas_root (e_tree->priv->header_canvas), + e_table_header_item_get_type (), + "ETableHeader", e_tree->priv->header, + "full_header", e_tree->priv->full_header, + "sort_info", e_tree->priv->sort_info, + "dnd_code", pointer, + "tree", e_tree, + NULL); + + g_free (pointer); + + g_signal_connect ( + e_tree->priv->header_canvas, "size_allocate", + G_CALLBACK (header_canvas_size_allocate), e_tree); + + gtk_widget_set_size_request ( + GTK_WIDGET (e_tree->priv->header_canvas), -1, + E_TABLE_HEADER_ITEM (e_tree->priv->header_item)->height); +} + +static void +scroll_to_cursor (ETree *e_tree) +{ + ETreePath path; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + gint x, y, w, h; + gdouble page_size; + gdouble lower; + gdouble upper; + gdouble value; + + path = e_tree_get_cursor (e_tree); + x = y = w = h = 0; + + if (path) { + gint row = e_tree_row_of_node (e_tree, path); + gint col = 0; + + if (row >= 0) + e_table_item_get_cell_geometry ( + E_TABLE_ITEM (e_tree->priv->item), + &row, &col, &x, &y, &w, &h); + } + + scrollable = GTK_SCROLLABLE (e_tree->priv->table_canvas); + adjustment = gtk_scrollable_get_vadjustment (scrollable); + + page_size = gtk_adjustment_get_page_size (adjustment); + lower = gtk_adjustment_get_lower (adjustment); + upper = gtk_adjustment_get_upper (adjustment); + value = gtk_adjustment_get_value (adjustment); + + if (y < value || y + h > value + page_size) { + value = CLAMP (y - page_size / 2, lower, upper - page_size); + gtk_adjustment_set_value (adjustment, value); + } +} + +static gboolean +tree_canvas_reflow_idle (ETree *e_tree) +{ + gdouble height, width; + gdouble oldheight, oldwidth; + GtkAllocation allocation; + GtkWidget *widget; + + widget = GTK_WIDGET (e_tree->priv->table_canvas); + gtk_widget_get_allocation (widget, &allocation); + + g_object_get ( + e_tree->priv->item, + "height", &height, "width", &width, NULL); + + height = MAX ((gint) height, allocation.height); + width = MAX ((gint) width, allocation.width); + + /* I have no idea why this needs to be -1, but it works. */ + gnome_canvas_get_scroll_region ( + GNOME_CANVAS (e_tree->priv->table_canvas), + NULL, NULL, &oldwidth, &oldheight); + + if (oldwidth != width - 1 || + oldheight != height - 1) { + gnome_canvas_set_scroll_region ( + GNOME_CANVAS (e_tree->priv->table_canvas), + 0, 0, width - 1, height - 1); + set_header_canvas_width (e_tree); + } + + e_tree->priv->reflow_idle_id = 0; + + if (e_tree->priv->show_cursor_after_reflow) { + e_tree->priv->show_cursor_after_reflow = FALSE; + scroll_to_cursor (e_tree); + } + + return FALSE; +} + +static void +tree_canvas_size_allocate (GtkWidget *widget, + GtkAllocation *alloc, + ETree *e_tree) +{ + gdouble width; + gdouble height; + GValue *val = g_new0 (GValue, 1); + g_value_init (val, G_TYPE_DOUBLE); + + width = alloc->width; + g_value_set_double (val, width); + g_object_get ( + e_tree->priv->item, + "height", &height, + NULL); + height = MAX ((gint) height, alloc->height); + + g_object_set ( + e_tree->priv->item, + "width", width, + NULL); + g_object_set_property (G_OBJECT (e_tree->priv->header), "width", val); + g_free (val); + + if (e_tree->priv->reflow_idle_id) + g_source_remove (e_tree->priv->reflow_idle_id); + tree_canvas_reflow_idle (e_tree); +} + +static void +tree_canvas_reflow (GnomeCanvas *canvas, + ETree *e_tree) +{ + if (!e_tree->priv->reflow_idle_id) + e_tree->priv->reflow_idle_id = g_idle_add_full ( + 400, (GSourceFunc) tree_canvas_reflow_idle, + e_tree, NULL); +} + +static void +item_cursor_change (ETableItem *eti, + gint row, + ETree *et) +{ + ETreePath path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + + g_signal_emit (et, et_signals[CURSOR_CHANGE], 0, row, path); +} + +static void +item_cursor_activated (ETableItem *eti, + gint row, + ETree *et) +{ + ETreePath path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + + g_signal_emit (et, et_signals[CURSOR_ACTIVATED], 0, row, path); +} + +static void +item_double_click (ETableItem *eti, + gint row, + gint col, + GdkEvent *event, + ETree *et) +{ + ETreePath path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + + g_signal_emit (et, et_signals[DOUBLE_CLICK], 0, row, path, col, event); +} + +static gboolean +item_right_click (ETableItem *eti, + gint row, + gint col, + GdkEvent *event, + ETree *et) +{ + ETreePath path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + gboolean return_val = 0; + + g_signal_emit ( + et, et_signals[RIGHT_CLICK], 0, + row, path, col, event, &return_val); + + return return_val; +} + +static gboolean +item_click (ETableItem *eti, + gint row, + gint col, + GdkEvent *event, + ETree *et) +{ + gboolean return_val = 0; + ETreePath path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + + g_signal_emit ( + et, et_signals[CLICK], 0, row, path, col, event, &return_val); + + return return_val; +} + +static gint +item_key_press (ETableItem *eti, + gint row, + gint col, + GdkEvent *event, + ETree *et) +{ + gint return_val = 0; + GdkEventKey *key = (GdkEventKey *) event; + ETreePath path; + gint y, row_local, col_local; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + gdouble page_size; + gdouble upper; + gdouble value; + + scrollable = GTK_SCROLLABLE (et->priv->table_canvas); + adjustment = gtk_scrollable_get_vadjustment (scrollable); + + page_size = gtk_adjustment_get_page_size (adjustment); + upper = gtk_adjustment_get_upper (adjustment); + value = gtk_adjustment_get_value (adjustment); + + switch (key->keyval) { + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + y = CLAMP (value + (2 * page_size - 50), 0, upper); + y -= value; + e_tree_get_cell_at (et, 30, y, &row_local, &col_local); + + if (row_local == -1) + row_local = e_table_model_row_count ( + E_TABLE_MODEL (et->priv->etta)) - 1; + + row_local = e_tree_view_to_model_row (et, row_local); + col_local = e_selection_model_cursor_col ( + E_SELECTION_MODEL (et->priv->selection)); + e_selection_model_select_as_key_press ( + E_SELECTION_MODEL (et->priv->selection), + row_local, col_local, key->state); + + return_val = 1; + break; + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + y = CLAMP (value - (page_size - 50), 0, upper); + y -= value; + e_tree_get_cell_at (et, 30, y, &row_local, &col_local); + + if (row_local == -1) + row_local = e_table_model_row_count ( + E_TABLE_MODEL (et->priv->etta)) - 1; + + row_local = e_tree_view_to_model_row (et, row_local); + col_local = e_selection_model_cursor_col ( + E_SELECTION_MODEL (et->priv->selection)); + e_selection_model_select_as_key_press ( + E_SELECTION_MODEL (et->priv->selection), + row_local, col_local, key->state); + + return_val = 1; + break; + case GDK_KEY_plus: + case GDK_KEY_KP_Add: + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + /* Only allow if the Shift modifier is used. + * eg. Ctrl-Equal shouldn't be handled. */ + if ((key->state & (GDK_SHIFT_MASK | GDK_LOCK_MASK | + GDK_MOD1_MASK)) != GDK_SHIFT_MASK) + break; + if (row != -1) { + path = e_tree_table_adapter_node_at_row ( + et->priv->etta, row); + if (path) + e_tree_table_adapter_node_set_expanded ( + et->priv->etta, path, TRUE); + } + return_val = 1; + break; + case GDK_KEY_underscore: + case GDK_KEY_KP_Subtract: + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + /* Only allow if the Shift modifier is used. + * eg. Ctrl-Minus shouldn't be handled. */ + if ((key->state & (GDK_SHIFT_MASK | GDK_LOCK_MASK | + GDK_MOD1_MASK)) != GDK_SHIFT_MASK) + break; + if (row != -1) { + path = e_tree_table_adapter_node_at_row ( + et->priv->etta, row); + if (path) + e_tree_table_adapter_node_set_expanded ( + et->priv->etta, path, FALSE); + } + return_val = 1; + break; + case GDK_KEY_BackSpace: + if (e_table_search_backspace (et->priv->search)) + return TRUE; + /* Fallthrough */ + default: + if ((key->state & ~(GDK_SHIFT_MASK | GDK_LOCK_MASK | + GDK_MOD1_MASK | GDK_MOD2_MASK | GDK_MOD3_MASK | + GDK_MOD4_MASK | GDK_MOD5_MASK)) == 0 + && ((key->keyval >= GDK_KEY_a && key->keyval <= GDK_KEY_z) || + (key->keyval >= GDK_KEY_A && key->keyval <= GDK_KEY_Z) || + (key->keyval >= GDK_KEY_0 && key->keyval <= GDK_KEY_9))) { + e_table_search_input_character (et->priv->search, key->keyval); + } + path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + g_signal_emit ( + et, + et_signals[KEY_PRESS], 0, + row, path, col, event, &return_val); + break; + } + return return_val; +} + +static gint +item_start_drag (ETableItem *eti, + gint row, + gint col, + GdkEvent *event, + ETree *et) +{ + ETreePath path; + gint return_val = 0; + + path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + + g_signal_emit ( + et, et_signals[START_DRAG], 0, + row, path, col, event, &return_val); + + return return_val; +} + +static void +et_selection_model_selection_changed (ETableSelectionModel *etsm, + ETree *et) +{ + g_signal_emit (et, et_signals[SELECTION_CHANGE], 0); +} + +static void +et_selection_model_selection_row_changed (ETableSelectionModel *etsm, + gint row, + ETree *et) +{ + g_signal_emit (et, et_signals[SELECTION_CHANGE], 0); +} + +static void +et_build_item (ETree *et) +{ + et->priv->item = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP ( + gnome_canvas_root (et->priv->table_canvas)), + e_table_item_get_type (), + "ETableHeader", et->priv->header, + "ETableModel", et->priv->etta, + "selection_model", et->priv->selection, + "alternating_row_colors", et->priv->alternating_row_colors, + "horizontal_draw_grid", et->priv->horizontal_draw_grid, + "vertical_draw_grid", et->priv->vertical_draw_grid, + "drawfocus", et->priv->draw_focus, + "cursor_mode", et->priv->cursor_mode, + "length_threshold", et->priv->length_threshold, + "uniform_row_height", et->priv->uniform_row_height, + NULL); + + g_signal_connect ( + et->priv->item, "cursor_change", + G_CALLBACK (item_cursor_change), et); + g_signal_connect ( + et->priv->item, "cursor_activated", + G_CALLBACK (item_cursor_activated), et); + g_signal_connect ( + et->priv->item, "double_click", + G_CALLBACK (item_double_click), et); + g_signal_connect ( + et->priv->item, "right_click", + G_CALLBACK (item_right_click), et); + g_signal_connect ( + et->priv->item, "click", + G_CALLBACK (item_click), et); + g_signal_connect ( + et->priv->item, "key_press", + G_CALLBACK (item_key_press), et); + g_signal_connect ( + et->priv->item, "start_drag", + G_CALLBACK (item_start_drag), et); +} + +static void +et_canvas_style_set (GtkWidget *widget, + GtkStyle *prev_style) +{ + GtkStyle *style; + + style = gtk_widget_get_style (widget); + + gnome_canvas_item_set ( + E_TREE (widget)->priv->white_item, + "fill_color_gdk", &style->base[GTK_STATE_NORMAL], + NULL); +} + +static gboolean +white_item_event (GnomeCanvasItem *white_item, + GdkEvent *event, + ETree *e_tree) +{ + gboolean return_val = 0; + g_signal_emit ( + e_tree, + et_signals[WHITE_SPACE_EVENT], 0, + event, &return_val); + return return_val; +} + +static gint +et_canvas_root_event (GnomeCanvasItem *root, + GdkEvent *event, + ETree *e_tree) +{ + switch (event->type) { + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + if (event->button.button != 4 && event->button.button != 5) { + if (gtk_widget_has_focus (GTK_WIDGET (root->canvas))) { + GnomeCanvasItem *item = GNOME_CANVAS (root->canvas)->focused_item; + + if (E_IS_TABLE_ITEM (item)) { + e_table_item_leave_edit (E_TABLE_ITEM (item)); + return TRUE; + } + } + } + break; + default: + break; + } + + return FALSE; +} + +/* Handler for focus events in the table_canvas; we have to repaint ourselves + * and give the focus to some ETableItem. + */ +static gboolean +table_canvas_focus_event_cb (GtkWidget *widget, + GdkEventFocus *event, + gpointer data) +{ + GnomeCanvas *canvas; + ETree *tree; + + gtk_widget_queue_draw (widget); + + if (!event->in) + return TRUE; + + canvas = GNOME_CANVAS (widget); + tree = E_TREE (data); + + if (!canvas->focused_item || + (e_selection_model_cursor_row (tree->priv->selection) == -1)) { + e_table_item_set_cursor (E_TABLE_ITEM (tree->priv->item), 0, 0); + gnome_canvas_item_grab_focus (tree->priv->item); + } + + return TRUE; +} + +static void +e_tree_setup_table (ETree *e_tree) +{ + GtkWidget *widget; + GtkStyle *style; + + e_tree->priv->table_canvas = GNOME_CANVAS (e_canvas_new ()); + g_signal_connect ( + e_tree->priv->table_canvas, "size_allocate", + G_CALLBACK (tree_canvas_size_allocate), e_tree); + g_signal_connect ( + e_tree->priv->table_canvas, "focus_in_event", + G_CALLBACK (table_canvas_focus_event_cb), e_tree); + g_signal_connect ( + e_tree->priv->table_canvas, "focus_out_event", + G_CALLBACK (table_canvas_focus_event_cb), e_tree); + + g_signal_connect ( + e_tree->priv->table_canvas, "drag_begin", + G_CALLBACK (et_drag_begin), e_tree); + g_signal_connect ( + e_tree->priv->table_canvas, "drag_end", + G_CALLBACK (et_drag_end), e_tree); + g_signal_connect ( + e_tree->priv->table_canvas, "drag_data_get", + G_CALLBACK (et_drag_data_get), e_tree); + g_signal_connect ( + e_tree->priv->table_canvas, "drag_data_delete", + G_CALLBACK (et_drag_data_delete), e_tree); + g_signal_connect ( + e_tree, "drag_motion", + G_CALLBACK (et_drag_motion), e_tree); + g_signal_connect ( + e_tree, "drag_leave", + G_CALLBACK (et_drag_leave), e_tree); + g_signal_connect ( + e_tree, "drag_drop", + G_CALLBACK (et_drag_drop), e_tree); + g_signal_connect ( + e_tree, "drag_data_received", + G_CALLBACK (et_drag_data_received), e_tree); + + g_signal_connect ( + e_tree->priv->table_canvas, "reflow", + G_CALLBACK (tree_canvas_reflow), e_tree); + + widget = GTK_WIDGET (e_tree->priv->table_canvas); + style = gtk_widget_get_style (widget); + + gtk_widget_show (widget); + + e_tree->priv->white_item = gnome_canvas_item_new ( + gnome_canvas_root (e_tree->priv->table_canvas), + e_canvas_background_get_type (), + "fill_color_gdk", &style->base[GTK_STATE_NORMAL], + NULL); + + g_signal_connect ( + e_tree->priv->white_item, "event", + G_CALLBACK (white_item_event), e_tree); + g_signal_connect ( + gnome_canvas_root (e_tree->priv->table_canvas), "event", + G_CALLBACK (et_canvas_root_event), e_tree); + + et_build_item (e_tree); +} + +/** + * e_tree_set_search_column: + * @e_tree: #ETree object that will be modified + * @col: Column index to use for searches + * + * This routine sets the current search column to be used for keypress + * searches of the #ETree. If -1 is passed in for column, the current + * search column is cleared. + */ +void +e_tree_set_search_column (ETree *e_tree, + gint col) +{ + if (col == -1) { + clear_current_search_col (e_tree); + return; + } + + e_tree->priv->search_col_set = TRUE; + e_tree->priv->current_search_col = e_table_header_get_column ( + e_tree->priv->full_header, col); +} + +void +e_tree_set_state_object (ETree *e_tree, + ETableState *state) +{ + GValue *val; + GtkAllocation allocation; + GtkWidget *widget; + + val = g_new0 (GValue, 1); + g_value_init (val, G_TYPE_DOUBLE); + + connect_header (e_tree, state); + + widget = GTK_WIDGET (e_tree->priv->table_canvas); + gtk_widget_get_allocation (widget, &allocation); + + g_value_set_double (val, (gdouble) allocation.width); + g_object_set_property (G_OBJECT (e_tree->priv->header), "width", val); + g_free (val); + + if (e_tree->priv->header_item) + g_object_set ( + e_tree->priv->header_item, + "ETableHeader", e_tree->priv->header, + "sort_info", e_tree->priv->sort_info, + NULL); + + if (e_tree->priv->item) + g_object_set ( + e_tree->priv->item, + "ETableHeader", e_tree->priv->header, + NULL); + + if (e_tree->priv->etta) + e_tree_table_adapter_set_sort_info ( + e_tree->priv->etta, e_tree->priv->sort_info); + + e_tree_state_change (e_tree); +} + +/** + * e_tree_set_state: + * @e_tree: #ETree object that will be modified + * @state_str: a string with the XML representation of the #ETableState. + * + * This routine sets the state (as described by #ETableState) of the + * #ETree object. + */ +void +e_tree_set_state (ETree *e_tree, + const gchar *state_str) +{ + ETableState *state; + + g_return_if_fail (e_tree != NULL); + g_return_if_fail (E_IS_TREE (e_tree)); + g_return_if_fail (state_str != NULL); + + state = e_table_state_new (); + e_table_state_load_from_string (state, state_str); + + if (state->col_count > 0) + e_tree_set_state_object (e_tree, state); + + g_object_unref (state); +} + +/** + * e_tree_load_state: + * @e_tree: #ETree object that will be modified + * @filename: name of the file containing the state to be loaded into the #ETree + * + * An #ETableState will be loaded form the file pointed by @filename into the + * @e_tree object. + */ +void +e_tree_load_state (ETree *e_tree, + const gchar *filename) +{ + ETableState *state; + + g_return_if_fail (e_tree != NULL); + g_return_if_fail (E_IS_TREE (e_tree)); + g_return_if_fail (filename != NULL); + + state = e_table_state_new (); + e_table_state_load_from_file (state, filename); + + if (state->col_count > 0) + e_tree_set_state_object (e_tree, state); + + g_object_unref (state); +} + +/** + * e_tree_get_state_object: + * @e_tree: #ETree object to act on + * + * Builds an #ETableState corresponding to the current state of the + * #ETree. + * + * Return value: + * The %ETableState object generated. + **/ +ETableState * +e_tree_get_state_object (ETree *e_tree) +{ + ETableState *state; + gint full_col_count; + gint i, j; + + state = e_table_state_new (); + state->sort_info = e_tree->priv->sort_info; + if (state->sort_info) + g_object_ref (state->sort_info); + + state->col_count = e_table_header_count (e_tree->priv->header); + full_col_count = e_table_header_count (e_tree->priv->full_header); + state->columns = g_new (int, state->col_count); + state->expansions = g_new (double, state->col_count); + for (i = 0; i < state->col_count; i++) { + ETableCol *col = e_table_header_get_column (e_tree->priv->header, i); + state->columns[i] = -1; + for (j = 0; j < full_col_count; j++) { + if (col->col_idx == e_table_header_index (e_tree->priv->full_header, j)) { + state->columns[i] = j; + break; + } + } + state->expansions[i] = col->expansion; + } + + return state; +} + +/** + * e_tree_get_state: + * @e_tree: The #ETree to act on + * + * Builds a state object based on the current state and returns the + * string corresponding to that state. + * + * Return value: + * A string describing the current state of the #ETree. + **/ +gchar * +e_tree_get_state (ETree *e_tree) +{ + ETableState *state; + gchar *string; + + state = e_tree_get_state_object (e_tree); + string = e_table_state_save_to_string (state); + g_object_unref (state); + return string; +} + +/** + * e_tree_save_state: + * @e_tree: The #ETree to act on + * @filename: name of the file to save to + * + * Saves the state of the @e_tree object into the file pointed by + * @filename. + **/ +void +e_tree_save_state (ETree *e_tree, + const gchar *filename) +{ + ETableState *state; + + state = e_tree_get_state_object (e_tree); + e_table_state_save_to_file (state, filename); + g_object_unref (state); +} + +/** + * e_tree_get_spec: + * @e_tree: The #ETree to query + * + * Returns the specification object. + * + * Return value: + **/ +ETableSpecification * +e_tree_get_spec (ETree *e_tree) +{ + return e_tree->priv->spec; +} + +static void +et_table_model_changed (ETableModel *model, + ETree *et) +{ + if (et->priv->horizontal_scrolling) + e_table_header_update_horizontal (et->priv->header); +} + +static void +et_table_row_changed (ETableModel *table_model, + gint row, + ETree *et) +{ + et_table_model_changed (table_model, et); +} + +static void +et_table_cell_changed (ETableModel *table_model, + gint view_col, + gint row, + ETree *et) +{ + et_table_model_changed (table_model, et); +} + +static void +et_table_rows_deleted (ETableModel *table_model, + gint row, + gint count, + ETree *et) +{ + ETreePath * node, * prev_node; + + /* If the cursor is still valid after this deletion, we're done */ + if (e_selection_model_cursor_row (et->priv->selection) >= 0 + || row == 0) + return; + + prev_node = e_tree_node_at_row (et, row - 1); + node = e_tree_get_cursor (et); + + /* Check if the cursor is a child of the node directly before the + * deleted region (implying that an expander was collapsed with + * the cursor inside it) */ + while (node) { + node = e_tree_model_node_get_parent (et->priv->model, node); + if (node == prev_node) { + /* Set the cursor to the still-visible parent */ + e_tree_set_cursor (et, prev_node); + return; + } + } +} + +static void +et_connect_to_etta (ETree *et) +{ + et->priv->table_model_change_id = g_signal_connect ( + et->priv->etta, "model_changed", + G_CALLBACK (et_table_model_changed), et); + + et->priv->table_row_change_id = g_signal_connect ( + et->priv->etta, "model_row_changed", + G_CALLBACK (et_table_row_changed), et); + + et->priv->table_cell_change_id = g_signal_connect ( + et->priv->etta, "model_cell_changed", + G_CALLBACK (et_table_cell_changed), et); + + et->priv->table_rows_delete_id = g_signal_connect ( + et->priv->etta, "model_rows_deleted", + G_CALLBACK (et_table_rows_deleted), et); + +} + +static gboolean +et_real_construct (ETree *e_tree, + ETreeModel *etm, + ETableExtras *ete, + ETableSpecification *specification, + ETableState *state) +{ + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + gint row = 0; + + if (ete) + g_object_ref (ete); + else + ete = e_table_extras_new (); + + e_tree->priv->alternating_row_colors = specification->alternating_row_colors; + e_tree->priv->horizontal_draw_grid = specification->horizontal_draw_grid; + e_tree->priv->vertical_draw_grid = specification->vertical_draw_grid; + e_tree->priv->draw_focus = specification->draw_focus; + e_tree->priv->cursor_mode = specification->cursor_mode; + e_tree->priv->full_header = e_table_spec_to_full_header (specification, ete); + + connect_header (e_tree, state); + + e_tree->priv->horizontal_scrolling = specification->horizontal_scrolling; + + e_tree->priv->model = etm; + g_object_ref (etm); + + e_tree->priv->etta = E_TREE_TABLE_ADAPTER ( + e_tree_table_adapter_new (e_tree->priv->model, + e_tree->priv->sort_info, e_tree->priv->full_header)); + + et_connect_to_etta (e_tree); + + e_tree->priv->sorter = e_sorter_new (); + + g_object_set ( + e_tree->priv->selection, + "sorter", e_tree->priv->sorter, +#ifdef E_TREE_USE_TREE_SELECTION + "model", e_tree->priv->model, + "etta", e_tree->priv->etta, +#else + "model", e_tree->priv->etta, +#endif + "selection_mode", specification->selection_mode, + "cursor_mode", specification->cursor_mode, + NULL); + + g_signal_connect ( + e_tree->priv->selection, "selection_changed", + G_CALLBACK (et_selection_model_selection_changed), e_tree); + g_signal_connect ( + e_tree->priv->selection, "selection_row_changed", + G_CALLBACK (et_selection_model_selection_row_changed), e_tree); + + if (!specification->no_headers) { + e_tree_setup_header (e_tree); + } + e_tree_setup_table (e_tree); + + scrollable = GTK_SCROLLABLE (e_tree->priv->table_canvas); + + adjustment = gtk_scrollable_get_vadjustment (scrollable); + gtk_adjustment_set_step_increment (adjustment, 20); + + adjustment = gtk_scrollable_get_hadjustment (scrollable); + gtk_adjustment_set_step_increment (adjustment, 20); + + if (!specification->no_headers) { + /* + * The header + */ + gtk_table_attach ( + GTK_TABLE (e_tree), + GTK_WIDGET (e_tree->priv->header_canvas), + 0, 1, 0 + row, 1 + row, + GTK_FILL | GTK_EXPAND, + GTK_FILL, 0, 0); + row++; + } + + gtk_table_attach ( + GTK_TABLE (e_tree), + GTK_WIDGET (e_tree->priv->table_canvas), + 0, 1, 0 + row, 1 + row, + GTK_FILL | GTK_EXPAND, + GTK_FILL | GTK_EXPAND, + 0, 0); + + g_object_unref (ete); + + return TRUE; +} + +/** + * e_tree_construct: + * @e_tree: The newly created #ETree object. + * @etm: The model for this table. + * @ete: An optional #ETableExtras. (%NULL is valid.) + * @spec_str: The spec. + * @state_str: An optional state. (%NULL is valid.) + * + * This is the internal implementation of e_tree_new() for use by + * subclasses or language bindings. See e_tree_new() for details. + * + * Return value: %TRUE on success, %FALSE if an error occurred + **/ +gboolean +e_tree_construct (ETree *e_tree, + ETreeModel *etm, + ETableExtras *ete, + const gchar *spec_str, + const gchar *state_str) +{ + ETableSpecification *specification; + ETableState *state; + + g_return_val_if_fail (e_tree != NULL, FALSE); + g_return_val_if_fail (E_IS_TREE (e_tree), FALSE); + g_return_val_if_fail (etm != NULL, FALSE); + g_return_val_if_fail (E_IS_TREE_MODEL (etm), FALSE); + g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), FALSE); + g_return_val_if_fail (spec_str != NULL, FALSE); + + specification = e_table_specification_new (); + if (!e_table_specification_load_from_string (specification, spec_str)) { + g_object_unref (specification); + return FALSE; + } + if (state_str) { + state = e_table_state_new (); + e_table_state_load_from_string (state, state_str); + if (state->col_count <= 0) { + g_object_unref (state); + state = specification->state; + g_object_ref (state); + } + } else { + state = specification->state; + g_object_ref (state); + } + + if (!et_real_construct (e_tree, etm, ete, specification, state)) { + g_object_unref (specification); + g_object_unref (state); + return FALSE; + } + + e_tree->priv->spec = specification; + e_tree->priv->spec->allow_grouping = FALSE; + + g_object_unref (state); + + return TRUE; +} + +/** + * e_tree_construct_from_spec_file: + * @e_tree: The newly created #ETree object. + * @etm: The model for this tree + * @ete: An optional #ETableExtras (%NULL is valid.) + * @spec_fn: The filename of the spec + * @state_fn: An optional state file (%NULL is valid.) + * + * This is the internal implementation of e_tree_new_from_spec_file() + * for use by subclasses or language bindings. See + * e_tree_new_from_spec_file() for details. + * + * Return value: %TRUE on success, %FALSE if an error occurred + **/ +gboolean +e_tree_construct_from_spec_file (ETree *e_tree, + ETreeModel *etm, + ETableExtras *ete, + const gchar *spec_fn, + const gchar *state_fn) +{ + ETableSpecification *specification; + ETableState *state; + + g_return_val_if_fail (e_tree != NULL, FALSE); + g_return_val_if_fail (E_IS_TREE (e_tree), FALSE); + g_return_val_if_fail (etm != NULL, FALSE); + g_return_val_if_fail (E_IS_TREE_MODEL (etm), FALSE); + g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), FALSE); + g_return_val_if_fail (spec_fn != NULL, FALSE); + + specification = e_table_specification_new (); + if (!e_table_specification_load_from_file (specification, spec_fn)) { + g_object_unref (specification); + return FALSE; + } + if (state_fn) { + state = e_table_state_new (); + if (!e_table_state_load_from_file (state, state_fn)) { + g_object_unref (state); + state = specification->state; + g_object_ref (state); + } + if (state->col_count <= 0) { + g_object_unref (state); + state = specification->state; + g_object_ref (state); + } + } else { + state = specification->state; + g_object_ref (state); + } + + if (!et_real_construct (e_tree, etm, ete, specification, state)) { + g_object_unref (specification); + g_object_unref (state); + return FALSE; + } + + e_tree->priv->spec = specification; + e_tree->priv->spec->allow_grouping = FALSE; + + g_object_unref (state); + + return TRUE; +} + +/** + * e_tree_new: + * @etm: The model for this tree + * @ete: An optional #ETableExtras (%NULL is valid.) + * @spec: The spec + * @state: An optional state (%NULL is valid.) + * + * This function creates an #ETree from the given parameters. The + * #ETreeModel is a tree model to be represented. The #ETableExtras + * is an optional set of pixbufs, cells, and sorting functions to be + * used when interpreting the spec. If you pass in %NULL it uses the + * default #ETableExtras. (See e_table_extras_new()). + * + * @spec is the specification of the set of viewable columns and the + * default sorting state and such. @state is an optional string + * specifying the current sorting state and such. If @state is NULL, + * then the default state from the spec will be used. + * + * Return value: + * The newly created #ETree or %NULL if there's an error. + **/ +GtkWidget * +e_tree_new (ETreeModel *etm, + ETableExtras *ete, + const gchar *spec, + const gchar *state) +{ + ETree *e_tree; + + g_return_val_if_fail (etm != NULL, NULL); + g_return_val_if_fail (E_IS_TREE_MODEL (etm), NULL); + g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), NULL); + g_return_val_if_fail (spec != NULL, NULL); + + e_tree = g_object_new (E_TYPE_TREE, NULL); + + if (!e_tree_construct (e_tree, etm, ete, spec, state)) { + g_object_unref (e_tree); + return NULL; + } + + return (GtkWidget *) e_tree; +} + +/** + * e_tree_new_from_spec_file: + * @etm: The model for this tree. + * @ete: An optional #ETableExtras. (%NULL is valid.) + * @spec_fn: The filename of the spec. + * @state_fn: An optional state file. (%NULL is valid.) + * + * This is very similar to e_tree_new(), except instead of passing in + * strings you pass in the file names of the spec and state to load. + * + * @spec_fn is the filename of the spec to load. If this file doesn't + * exist, e_tree_new_from_spec_file will return %NULL. + * + * @state_fn is the filename of the initial state to load. If this is + * %NULL or if the specified file doesn't exist, the default state + * from the spec file is used. + * + * Return value: + * The newly created #ETree or %NULL if there's an error. + **/ +GtkWidget * +e_tree_new_from_spec_file (ETreeModel *etm, + ETableExtras *ete, + const gchar *spec_fn, + const gchar *state_fn) +{ + ETree *e_tree; + + g_return_val_if_fail (etm != NULL, NULL); + g_return_val_if_fail (E_IS_TREE_MODEL (etm), NULL); + g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), NULL); + g_return_val_if_fail (spec_fn != NULL, NULL); + + e_tree = g_object_new (E_TYPE_TREE, NULL); + + if (!e_tree_construct_from_spec_file (e_tree, etm, ete, spec_fn, state_fn)) { + g_object_unref (e_tree); + return NULL; + } + + return (GtkWidget *) e_tree; +} + +void +e_tree_show_cursor_after_reflow (ETree *e_tree) +{ + g_return_if_fail (e_tree != NULL); + g_return_if_fail (E_IS_TREE (e_tree)); + + e_tree->priv->show_cursor_after_reflow = TRUE; +} + +void +e_tree_set_cursor (ETree *e_tree, + ETreePath path) +{ +#ifndef E_TREE_USE_TREE_SELECTION + gint row; +#endif + g_return_if_fail (e_tree != NULL); + g_return_if_fail (E_IS_TREE (e_tree)); + g_return_if_fail (path != NULL); + +#ifdef E_TREE_USE_TREE_SELECTION + e_tree_selection_model_select_single_path ( + E_TREE_SELECTION_MODEL (e_tree->priv->selection), path); + e_tree_selection_model_change_cursor ( + E_TREE_SELECTION_MODEL (e_tree->priv->selection), path); +#else + row = e_tree_table_adapter_row_of_node ( + E_TREE_TABLE_ADAPTER (e_tree->priv->etta), path); + + if (row == -1) + return; + + g_object_set ( + e_tree->priv->selection, + "cursor_row", row, + NULL); +#endif +} + +ETreePath +e_tree_get_cursor (ETree *e_tree) +{ +#ifdef E_TREE_USE_TREE_SELECTION + return e_tree_selection_model_get_cursor ( + E_TREE_SELECTION_MODEL (e_tree->priv->selection)); +#else + gint row; + g_return_val_if_fail (e_tree != NULL, NULL); + g_return_val_if_fail (E_IS_TREE (e_tree), NULL); + + g_object_get ( + e_tree->priv->selection, + "cursor_row", &row, + NULL); + if (row == -1) + return NULL; + + return e_tree_table_adapter_node_at_row ( + E_TREE_TABLE_ADAPTER (e_tree->priv->etta), row); +#endif +} + +void +e_tree_selected_row_foreach (ETree *e_tree, + EForeachFunc callback, + gpointer closure) +{ + g_return_if_fail (e_tree != NULL); + g_return_if_fail (E_IS_TREE (e_tree)); + + e_selection_model_foreach (e_tree->priv->selection, + callback, + closure); +} + +#ifdef E_TREE_USE_TREE_SELECTION +void +e_tree_selected_path_foreach (ETree *e_tree, + ETreeForeachFunc callback, + gpointer closure) +{ + g_return_if_fail (e_tree != NULL); + g_return_if_fail (E_IS_TREE (e_tree)); + + e_tree_selection_model_foreach ( + E_TREE_SELECTION_MODEL (e_tree->priv->selection), + callback, closure); +} + +/* Standard functions */ +static void +et_foreach_recurse (ETreeModel *model, + ETreePath path, + ETreeForeachFunc callback, + gpointer closure) +{ + ETreePath child; + + callback (path, closure); + + child = e_tree_model_node_get_first_child (E_TREE_MODEL (model), path); + for (; child; child = e_tree_model_node_get_next (E_TREE_MODEL (model), child)) + if (child) + et_foreach_recurse (model, child, callback, closure); +} + +void +e_tree_path_foreach (ETree *e_tree, + ETreeForeachFunc callback, + gpointer closure) +{ + ETreePath root; + + g_return_if_fail (e_tree != NULL); + g_return_if_fail (E_IS_TREE (e_tree)); + + root = e_tree_model_get_root (e_tree->priv->model); + + if (root) + et_foreach_recurse (e_tree->priv->model, + root, + callback, + closure); +} +#endif + +EPrintable * +e_tree_get_printable (ETree *e_tree) +{ + g_return_val_if_fail (e_tree != NULL, NULL); + g_return_val_if_fail (E_IS_TREE (e_tree), NULL); + + return e_table_item_get_printable (E_TABLE_ITEM (e_tree->priv->item)); +} + +static void +et_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ETree *etree = E_TREE (object); + + switch (property_id) { + case PROP_ETTA: + g_value_set_object (value, etree->priv->etta); + break; + + case PROP_UNIFORM_ROW_HEIGHT: + g_value_set_boolean (value, etree->priv->uniform_row_height); + break; + + case PROP_ALWAYS_SEARCH: + g_value_set_boolean (value, etree->priv->always_search); + break; + + case PROP_HADJUSTMENT: + if (etree->priv->table_canvas) + g_object_get_property ( + G_OBJECT (etree->priv->table_canvas), + "hadjustment", value); + else + g_value_set_object (value, NULL); + break; + + case PROP_VADJUSTMENT: + if (etree->priv->table_canvas) + g_object_get_property ( + G_OBJECT (etree->priv->table_canvas), + "vadjustment", value); + else + g_value_set_object (value, NULL); + break; + + case PROP_HSCROLL_POLICY: + if (etree->priv->table_canvas) + g_object_get_property ( + G_OBJECT (etree->priv->table_canvas), + "hscroll-policy", value); + else + g_value_set_enum (value, 0); + break; + + case PROP_VSCROLL_POLICY: + if (etree->priv->table_canvas) + g_object_get_property ( + G_OBJECT (etree->priv->table_canvas), + "vscroll-policy", value); + else + g_value_set_enum (value, 0); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +typedef struct { + gchar *arg; + gboolean setting; +} bool_closure; + +static void +et_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ETree *etree = E_TREE (object); + + switch (property_id) { + case PROP_LENGTH_THRESHOLD: + etree->priv->length_threshold = g_value_get_int (value); + if (etree->priv->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etree->priv->item), + "length_threshold", + etree->priv->length_threshold, + NULL); + } + break; + + case PROP_HORIZONTAL_DRAW_GRID: + etree->priv->horizontal_draw_grid = g_value_get_boolean (value); + if (etree->priv->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etree->priv->item), + "horizontal_draw_grid", + etree->priv->horizontal_draw_grid, + NULL); + } + break; + + case PROP_VERTICAL_DRAW_GRID: + etree->priv->vertical_draw_grid = g_value_get_boolean (value); + if (etree->priv->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etree->priv->item), + "vertical_draw_grid", + etree->priv->vertical_draw_grid, + NULL); + } + break; + + case PROP_DRAW_FOCUS: + etree->priv->draw_focus = g_value_get_boolean (value); + if (etree->priv->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etree->priv->item), + "drawfocus", + etree->priv->draw_focus, + NULL); + } + break; + + case PROP_UNIFORM_ROW_HEIGHT: + etree->priv->uniform_row_height = g_value_get_boolean (value); + if (etree->priv->item) { + gnome_canvas_item_set ( + GNOME_CANVAS_ITEM (etree->priv->item), + "uniform_row_height", + etree->priv->uniform_row_height, + NULL); + } + break; + + case PROP_ALWAYS_SEARCH: + if (etree->priv->always_search == g_value_get_boolean (value)) + return; + etree->priv->always_search = g_value_get_boolean (value); + clear_current_search_col (etree); + break; + + case PROP_HADJUSTMENT: + if (etree->priv->table_canvas) + g_object_set_property ( + G_OBJECT (etree->priv->table_canvas), + "hadjustment", value); + break; + + case PROP_VADJUSTMENT: + if (etree->priv->table_canvas) + g_object_set_property ( + G_OBJECT (etree->priv->table_canvas), + "vadjustment", value); + break; + + case PROP_HSCROLL_POLICY: + if (etree->priv->table_canvas) + g_object_set_property ( + G_OBJECT (etree->priv->table_canvas), + "hscroll-policy", value); + break; + + case PROP_VSCROLL_POLICY: + if (etree->priv->table_canvas) + g_object_set_property ( + G_OBJECT (etree->priv->table_canvas), + "vscroll-policy", value); + break; + } +} + +gint +e_tree_get_next_row (ETree *e_tree, + gint model_row) +{ + g_return_val_if_fail (e_tree != NULL, -1); + g_return_val_if_fail (E_IS_TREE (e_tree), -1); + + if (e_tree->priv->sorter) { + gint i; + i = e_sorter_model_to_sorted (E_SORTER (e_tree->priv->sorter), model_row); + i++; + if (i < e_table_model_row_count (E_TABLE_MODEL (e_tree->priv->etta))) { + return e_sorter_sorted_to_model (E_SORTER (e_tree->priv->sorter), i); + } else + return -1; + } else { + gint row_count; + + row_count = e_table_model_row_count ( + E_TABLE_MODEL (e_tree->priv->etta)); + + if (model_row < row_count - 1) + return model_row + 1; + else + return -1; + } +} + +gint +e_tree_get_prev_row (ETree *e_tree, + gint model_row) +{ + g_return_val_if_fail (e_tree != NULL, -1); + g_return_val_if_fail (E_IS_TREE (e_tree), -1); + + if (e_tree->priv->sorter) { + gint i; + i = e_sorter_model_to_sorted (E_SORTER (e_tree->priv->sorter), model_row); + i--; + if (i >= 0) + return e_sorter_sorted_to_model (E_SORTER (e_tree->priv->sorter), i); + else + return -1; + } else + return model_row - 1; +} + +gint +e_tree_model_to_view_row (ETree *e_tree, + gint model_row) +{ + g_return_val_if_fail (e_tree != NULL, -1); + g_return_val_if_fail (E_IS_TREE (e_tree), -1); + + if (e_tree->priv->sorter) + return e_sorter_model_to_sorted (E_SORTER (e_tree->priv->sorter), model_row); + else + return model_row; +} + +gint +e_tree_view_to_model_row (ETree *e_tree, + gint view_row) +{ + g_return_val_if_fail (e_tree != NULL, -1); + g_return_val_if_fail (E_IS_TREE (e_tree), -1); + + if (e_tree->priv->sorter) + return e_sorter_sorted_to_model (E_SORTER (e_tree->priv->sorter), view_row); + else + return view_row; +} + +gboolean +e_tree_node_is_expanded (ETree *et, + ETreePath path) +{ + g_return_val_if_fail (path, FALSE); + + return e_tree_table_adapter_node_is_expanded (et->priv->etta, path); +} + +void +e_tree_node_set_expanded (ETree *et, + ETreePath path, + gboolean expanded) +{ + g_return_if_fail (et != NULL); + g_return_if_fail (E_IS_TREE (et)); + + e_tree_table_adapter_node_set_expanded (et->priv->etta, path, expanded); +} + +void +e_tree_node_set_expanded_recurse (ETree *et, + ETreePath path, + gboolean expanded) +{ + g_return_if_fail (et != NULL); + g_return_if_fail (E_IS_TREE (et)); + + e_tree_table_adapter_node_set_expanded_recurse (et->priv->etta, path, expanded); +} + +void +e_tree_root_node_set_visible (ETree *et, + gboolean visible) +{ + g_return_if_fail (et != NULL); + g_return_if_fail (E_IS_TREE (et)); + + e_tree_table_adapter_root_node_set_visible (et->priv->etta, visible); +} + +ETreePath +e_tree_node_at_row (ETree *et, + gint row) +{ + ETreePath path = { 0 }; + + g_return_val_if_fail (et != NULL, path); + + path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + + return path; +} + +gint +e_tree_row_of_node (ETree *et, + ETreePath path) +{ + g_return_val_if_fail (et != NULL, -1); + + return e_tree_table_adapter_row_of_node (et->priv->etta, path); +} + +gboolean +e_tree_root_node_is_visible (ETree *et) +{ + g_return_val_if_fail (et != NULL, FALSE); + + return e_tree_table_adapter_root_node_is_visible (et->priv->etta); +} + +void +e_tree_show_node (ETree *et, + ETreePath path) +{ + g_return_if_fail (et != NULL); + g_return_if_fail (E_IS_TREE (et)); + + e_tree_table_adapter_show_node (et->priv->etta, path); +} + +void +e_tree_save_expanded_state (ETree *et, + gchar *filename) +{ + g_return_if_fail (et != NULL); + g_return_if_fail (E_IS_TREE (et)); + + e_tree_table_adapter_save_expanded_state (et->priv->etta, filename); +} + +void +e_tree_load_expanded_state (ETree *et, + gchar *filename) +{ + g_return_if_fail (et != NULL); + + e_tree_table_adapter_load_expanded_state (et->priv->etta, filename); +} + +xmlDoc * +e_tree_save_expanded_state_xml (ETree *et) +{ + g_return_val_if_fail (et != NULL, NULL); + g_return_val_if_fail (E_IS_TREE (et), NULL); + + return e_tree_table_adapter_save_expanded_state_xml (et->priv->etta); +} + +void +e_tree_load_expanded_state_xml (ETree *et, + xmlDoc *doc) +{ + g_return_if_fail (et != NULL); + g_return_if_fail (E_IS_TREE (et)); + g_return_if_fail (doc != NULL); + + e_tree_table_adapter_load_expanded_state_xml (et->priv->etta, doc); +} + +/* state: <0 ... collapse; 0 ... no force - use default; >0 ... expand; + * when using this, be sure to reset to 0 once no forcing is required + * anymore, aka the build of the tree is done */ +void +e_tree_force_expanded_state (ETree *et, + gint state) +{ + g_return_if_fail (et != NULL); + + e_tree_table_adapter_force_expanded_state (et->priv->etta, state); +} + +gint +e_tree_row_count (ETree *et) +{ + g_return_val_if_fail (et != NULL, -1); + + return e_table_model_row_count (E_TABLE_MODEL (et->priv->etta)); +} + +GtkWidget * +e_tree_get_tooltip (ETree *et) +{ + g_return_val_if_fail (et != NULL, NULL); + + return E_CANVAS (et->priv->table_canvas)->tooltip_window; +} + +static ETreePath +find_next_in_range (ETree *et, + gint start, + gint end, + ETreePathFunc func, + gpointer data) +{ + ETreePath path; + gint row; + + for (row = start; row <= end; row++) { + path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + if (path && func (et->priv->model, path, data)) + return path; + } + + return NULL; +} + +static ETreePath +find_prev_in_range (ETree *et, + gint start, + gint end, + ETreePathFunc func, + gpointer data) +{ + ETreePath path; + gint row; + + for (row = start; row >= end; row--) { + path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + if (path && func (et->priv->model, path, data)) + return path; + } + + return NULL; +} + +gboolean +e_tree_find_next (ETree *et, + ETreeFindNextParams params, + ETreePathFunc func, + gpointer data) +{ + ETreePath cursor, found; + gint row, row_count; + + cursor = e_tree_get_cursor (et); + row = e_tree_table_adapter_row_of_node (et->priv->etta, cursor); + row_count = e_table_model_row_count (E_TABLE_MODEL (et->priv->etta)); + + if (params & E_TREE_FIND_NEXT_FORWARD) + found = find_next_in_range (et, row + 1, row_count - 1, func, data); + else + found = find_prev_in_range (et, row == -1 ? -1 : row - 1, 0, func, data); + + if (found) { + e_tree_table_adapter_show_node (et->priv->etta, found); + e_tree_set_cursor (et, found); + return TRUE; + } + + if (params & E_TREE_FIND_NEXT_WRAP) { + if (params & E_TREE_FIND_NEXT_FORWARD) + found = find_next_in_range (et, 0, row, func, data); + else + found = find_prev_in_range (et, row_count - 1, row, func, data); + + if (found && found != cursor) { + e_tree_table_adapter_show_node (et->priv->etta, found); + e_tree_set_cursor (et, found); + return TRUE; + } + } + + return FALSE; +} + +void +e_tree_right_click_up (ETree *et) +{ + e_selection_model_right_click_up (et->priv->selection); +} + +/** + * e_tree_get_model: + * @et: the ETree + * + * Returns the model upon which this ETree is based. + * + * Returns: the model + **/ +ETreeModel * +e_tree_get_model (ETree *et) +{ + g_return_val_if_fail (et != NULL, NULL); + g_return_val_if_fail (E_IS_TREE (et), NULL); + + return et->priv->model; +} + +/** + * e_tree_get_selection_model: + * @et: the ETree + * + * Returns the selection model of this ETree. + * + * Returns: the selection model + **/ +ESelectionModel * +e_tree_get_selection_model (ETree *et) +{ + g_return_val_if_fail (et != NULL, NULL); + g_return_val_if_fail (E_IS_TREE (et), NULL); + + return et->priv->selection; +} + +/** + * e_tree_get_table_adapter: + * @et: the ETree + * + * Returns the table adapter this ETree uses. + * + * Returns: the model + **/ +ETreeTableAdapter * +e_tree_get_table_adapter (ETree *et) +{ + g_return_val_if_fail (et != NULL, NULL); + g_return_val_if_fail (E_IS_TREE (et), NULL); + + return et->priv->etta; +} + +ETableItem * +e_tree_get_item (ETree *et) +{ + g_return_val_if_fail (et != NULL, NULL); + g_return_val_if_fail (E_IS_TREE (et), NULL); + + return E_TABLE_ITEM (et->priv->item); +} + +GnomeCanvasItem * +e_tree_get_header_item (ETree *et) +{ + g_return_val_if_fail (et != NULL, NULL); + g_return_val_if_fail (E_IS_TREE (et), NULL); + + return et->priv->header_item; +} + +struct _ETreeDragSourceSite +{ + GdkModifierType start_button_mask; + GtkTargetList *target_list; /* Targets for drag data */ + GdkDragAction actions; /* Possible actions */ + GdkPixbuf *pixbuf; /* Icon for drag data */ + + /* Stored button press information to detect drag beginning */ + gint state; + gint x, y; + gint row, col; +}; + +typedef enum +{ + GTK_DRAG_STATUS_DRAG, + GTK_DRAG_STATUS_WAIT, + GTK_DRAG_STATUS_DROP +} GtkDragStatus; + +typedef struct _GtkDragDestInfo GtkDragDestInfo; +typedef struct _GtkDragSourceInfo GtkDragSourceInfo; + +struct _GtkDragDestInfo +{ + GtkWidget *widget; /* Widget in which drag is in */ + GdkDragContext *context; /* Drag context */ + GtkDragSourceInfo *proxy_source; /* Set if this is a proxy drag */ + GtkSelectionData *proxy_data; /* Set while retrieving proxied data */ + guint dropped : 1; /* Set after we receive a drop */ + guint32 proxy_drop_time; /* Timestamp for proxied drop */ + guint proxy_drop_wait : 1; /* Set if we are waiting for a + * status reply before sending + * a proxied drop on. + */ + gint drop_x, drop_y; /* Position of drop */ +}; + +struct _GtkDragSourceInfo +{ + GtkWidget *widget; + GtkTargetList *target_list; /* Targets for drag data */ + GdkDragAction possible_actions; /* Actions allowed by source */ + GdkDragContext *context; /* drag context */ + GtkWidget *icon_window; /* Window for drag */ + GtkWidget *ipc_widget; /* GtkInvisible for grab, message passing */ + GdkCursor *cursor; /* Cursor for drag */ + gint hot_x, hot_y; /* Hot spot for drag */ + gint button; /* mouse button starting drag */ + + GtkDragStatus status; /* drag status */ + GdkEvent *last_event; /* motion event waiting for response */ + + gint start_x, start_y; /* Initial position */ + gint cur_x, cur_y; /* Current Position */ + + GList *selections; /* selections we've claimed */ + + GtkDragDestInfo *proxy_dest; /* Set if this is a proxy drag */ + + guint drop_timeout; /* Timeout for aborting drop */ + guint destroy_icon : 1; /* If true, destroy icon_window + */ +}; + +/* Drag & drop stuff. */ +/* Target */ + +void +e_tree_drag_get_data (ETree *tree, + gint row, + gint col, + GdkDragContext *context, + GdkAtom target, + guint32 time) +{ + g_return_if_fail (tree != NULL); + g_return_if_fail (E_IS_TREE (tree)); + + gtk_drag_get_data ( + GTK_WIDGET (tree), + context, + target, + time); + +} + +/** + * e_tree_drag_highlight: + * @tree: + * @row: + * @col: + * + * Set col to -1 to highlight the entire row. + * Set row to -1 to turn off the highlight. + */ +void +e_tree_drag_highlight (ETree *tree, + gint row, + gint col) +{ + GtkAllocation allocation; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + GtkStyle *style; + + g_return_if_fail (E_IS_TREE (tree)); + + scrollable = GTK_SCROLLABLE (tree->priv->table_canvas); + style = gtk_widget_get_style (GTK_WIDGET (tree)); + gtk_widget_get_allocation (GTK_WIDGET (scrollable), &allocation); + + if (row != -1) { + gint x, y, width, height; + if (col == -1) { + e_tree_get_cell_geometry (tree, row, 0, &x, &y, &width, &height); + x = 0; + width = allocation.width; + } else { + e_tree_get_cell_geometry (tree, row, col, &x, &y, &width, &height); + adjustment = gtk_scrollable_get_hadjustment (scrollable); + x += gtk_adjustment_get_value (adjustment); + } + + adjustment = gtk_scrollable_get_vadjustment (scrollable); + y += gtk_adjustment_get_value (adjustment); + + if (tree->priv->drop_highlight == NULL) { + tree->priv->drop_highlight = gnome_canvas_item_new ( + gnome_canvas_root (tree->priv->table_canvas), + gnome_canvas_rect_get_type (), + "fill_color", NULL, + "outline_color_gdk", &style->fg[GTK_STATE_NORMAL], + NULL); + } + + gnome_canvas_item_set ( + tree->priv->drop_highlight, + "x1", (gdouble) x, + "x2", (gdouble) x + width - 1, + "y1", (gdouble) y, + "y2", (gdouble) y + height - 1, + NULL); + } else { + g_object_run_dispose (G_OBJECT (tree->priv->drop_highlight)); + tree->priv->drop_highlight = NULL; + } +} + +void +e_tree_drag_unhighlight (ETree *tree) +{ + g_return_if_fail (tree != NULL); + g_return_if_fail (E_IS_TREE (tree)); + + if (tree->priv->drop_highlight) { + g_object_run_dispose (G_OBJECT (tree->priv->drop_highlight)); + tree->priv->drop_highlight = NULL; + } +} + +void e_tree_drag_dest_set (ETree *tree, + GtkDestDefaults flags, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions) +{ + g_return_if_fail (tree != NULL); + g_return_if_fail (E_IS_TREE (tree)); + + gtk_drag_dest_set ( + GTK_WIDGET (tree), + flags, + targets, + n_targets, + actions); +} + +void e_tree_drag_dest_set_proxy (ETree *tree, + GdkWindow *proxy_window, + GdkDragProtocol protocol, + gboolean use_coordinates) +{ + g_return_if_fail (tree != NULL); + g_return_if_fail (E_IS_TREE (tree)); + + gtk_drag_dest_set_proxy ( + GTK_WIDGET (tree), + proxy_window, + protocol, + use_coordinates); +} + +/* + * There probably should be functions for setting the targets + * as a GtkTargetList + */ + +void +e_tree_drag_dest_unset (GtkWidget *widget) +{ + g_return_if_fail (widget != NULL); + g_return_if_fail (E_IS_TREE (widget)); + + gtk_drag_dest_unset (widget); +} + +/* Source side */ + +static gint +et_real_start_drag (ETree *tree, + gint row, + ETreePath path, + gint col, + GdkEvent *event) +{ + GtkDragSourceInfo *info; + GdkDragContext *context; + ETreeDragSourceSite *site; + + if (tree->priv->do_drag) { + site = tree->priv->site; + + site->state = 0; + context = e_tree_drag_begin ( + tree, row, col, + site->target_list, + site->actions, + 1, event); + + if (context) { + info = g_dataset_get_data (context, "gtk-info"); + + if (info && !info->icon_window) { + if (site->pixbuf) + gtk_drag_set_icon_pixbuf ( + context, + site->pixbuf, + -2, -2); + else + gtk_drag_set_icon_default (context); + } + } + return TRUE; + } + return FALSE; +} + +void +e_tree_drag_source_set (ETree *tree, + GdkModifierType start_button_mask, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions) +{ + ETreeDragSourceSite *site; + GtkWidget *canvas; + + g_return_if_fail (tree != NULL); + g_return_if_fail (E_IS_TREE (tree)); + + canvas = GTK_WIDGET (tree->priv->table_canvas); + site = tree->priv->site; + + tree->priv->do_drag = TRUE; + + gtk_widget_add_events ( + canvas, + gtk_widget_get_events (canvas) | + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_BUTTON_MOTION_MASK | GDK_STRUCTURE_MASK); + + if (site) { + if (site->target_list) + gtk_target_list_unref (site->target_list); + } else { + site = g_new0 (ETreeDragSourceSite, 1); + tree->priv->site = site; + } + + site->start_button_mask = start_button_mask; + + if (targets) + site->target_list = gtk_target_list_new (targets, n_targets); + else + site->target_list = NULL; + + site->actions = actions; +} + +void +e_tree_drag_source_unset (ETree *tree) +{ + ETreeDragSourceSite *site; + + g_return_if_fail (tree != NULL); + g_return_if_fail (E_IS_TREE (tree)); + + site = tree->priv->site; + + if (site) { + if (site->target_list) + gtk_target_list_unref (site->target_list); + g_free (site); + tree->priv->site = NULL; + } +} + +/* There probably should be functions for setting the targets + * as a GtkTargetList + */ + +GdkDragContext * +e_tree_drag_begin (ETree *tree, + gint row, + gint col, + GtkTargetList *targets, + GdkDragAction actions, + gint button, + GdkEvent *event) +{ + ETreePath path; + g_return_val_if_fail (tree != NULL, NULL); + g_return_val_if_fail (E_IS_TREE (tree), NULL); + + path = e_tree_table_adapter_node_at_row (tree->priv->etta, row); + + tree->priv->drag_row = row; + tree->priv->drag_path = path; + tree->priv->drag_col = col; + + return gtk_drag_begin ( + GTK_WIDGET (tree->priv->table_canvas), + targets, + actions, + button, + event); +} + +/** + * e_tree_is_dragging: + * @tree: An #ETree widget + * + * Returns whether is @tree in a drag&drop operation. + **/ +gboolean +e_tree_is_dragging (ETree *tree) +{ + g_return_val_if_fail (tree != NULL, FALSE); + g_return_val_if_fail (tree->priv != NULL, FALSE); + + return tree->priv->is_dragging; +} + +/** + * e_tree_get_cell_at: + * @tree: An ETree widget + * @x: X coordinate for the pixel + * @y: Y coordinate for the pixel + * @row_return: Pointer to return the row value + * @col_return: Pointer to return the column value + * + * Return the row and column for the cell in which the pixel at (@x, @y) is + * contained. + **/ +void +e_tree_get_cell_at (ETree *tree, + gint x, + gint y, + gint *row_return, + gint *col_return) +{ + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + + g_return_if_fail (E_IS_TREE (tree)); + g_return_if_fail (row_return != NULL); + g_return_if_fail (col_return != NULL); + + /* FIXME it would be nice if it could handle a NULL row_return or + * col_return gracefully. */ + + if (row_return) + *row_return = -1; + if (col_return) + *col_return = -1; + + scrollable = GTK_SCROLLABLE (tree->priv->table_canvas); + + adjustment = gtk_scrollable_get_hadjustment (scrollable); + x += gtk_adjustment_get_value (adjustment); + + adjustment = gtk_scrollable_get_vadjustment (scrollable); + y += gtk_adjustment_get_value (adjustment); + + e_table_item_compute_location ( + E_TABLE_ITEM (tree->priv->item), + &x, &y, row_return, col_return); +} + +/** + * e_tree_get_cell_geometry: + * @tree: The tree. + * @row: The row to get the geometry of. + * @col: The col to get the geometry of. + * @x_return: Returns the x coordinate of the upper right hand corner + * of the cell with respect to the widget. + * @y_return: Returns the y coordinate of the upper right hand corner + * of the cell with respect to the widget. + * @width_return: Returns the width of the cell. + * @height_return: Returns the height of the cell. + * + * Computes the data about this cell. + **/ +void +e_tree_get_cell_geometry (ETree *tree, + gint row, + gint col, + gint *x_return, + gint *y_return, + gint *width_return, + gint *height_return) +{ + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + + g_return_if_fail (E_IS_TREE (tree)); + g_return_if_fail (row >= 0); + g_return_if_fail (col >= 0); + + /* FIXME it would be nice if it could handle a NULL row_return or + * col_return gracefully. */ + + e_table_item_get_cell_geometry ( + E_TABLE_ITEM (tree->priv->item), + &row, &col, x_return, y_return, + width_return, height_return); + + scrollable = GTK_SCROLLABLE (tree->priv->table_canvas); + + if (x_return) { + adjustment = gtk_scrollable_get_hadjustment (scrollable); + (*x_return) -= gtk_adjustment_get_value (adjustment); + } + + if (y_return) { + adjustment = gtk_scrollable_get_vadjustment (scrollable); + (*y_return) -= gtk_adjustment_get_value (adjustment); + } +} + +static void +et_drag_begin (GtkWidget *widget, + GdkDragContext *context, + ETree *et) +{ + et->priv->is_dragging = TRUE; + + g_signal_emit ( + et, + et_signals[TREE_DRAG_BEGIN], 0, + et->priv->drag_row, + et->priv->drag_path, + et->priv->drag_col, + context); +} + +static void +et_drag_end (GtkWidget *widget, + GdkDragContext *context, + ETree *et) +{ + et->priv->is_dragging = FALSE; + + g_signal_emit ( + et, + et_signals[TREE_DRAG_END], 0, + et->priv->drag_row, + et->priv->drag_path, + et->priv->drag_col, + context); +} + +static void +et_drag_data_get (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time, + ETree *et) +{ + g_signal_emit ( + et, + et_signals[TREE_DRAG_DATA_GET], 0, + et->priv->drag_row, + et->priv->drag_path, + et->priv->drag_col, + context, + selection_data, + info, + time); +} + +static void +et_drag_data_delete (GtkWidget *widget, + GdkDragContext *context, + ETree *et) +{ + g_signal_emit ( + et, + et_signals[TREE_DRAG_DATA_DELETE], 0, + et->priv->drag_row, + et->priv->drag_path, + et->priv->drag_col, + context); +} + +static gboolean +do_drag_motion (ETree *et, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + gboolean ret_val = FALSE; + gint row, col; + ETreePath path; + + e_tree_get_cell_at (et, x, y, &row, &col); + + if (row != et->priv->drop_row && col != et->priv->drop_col) { + g_signal_emit ( + et, et_signals[TREE_DRAG_LEAVE], 0, + et->priv->drop_row, + et->priv->drop_path, + et->priv->drop_col, + context, + time); + } + + path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + + et->priv->drop_row = row; + et->priv->drop_path = path; + et->priv->drop_col = col; + g_signal_emit ( + et, et_signals[TREE_DRAG_MOTION], 0, + et->priv->drop_row, + et->priv->drop_path, + et->priv->drop_col, + context, + x, y, + time, + &ret_val); + + return ret_val; +} + +static gboolean +scroll_timeout (gpointer data) +{ + ETree *et = data; + gint dx = 0, dy = 0; + GtkAdjustment *adjustment; + GtkScrollable *scrollable; + gdouble old_h_value; + gdouble new_h_value; + gdouble old_v_value; + gdouble new_v_value; + gdouble page_size; + gdouble lower; + gdouble upper; + + if (et->priv->scroll_direction & ET_SCROLL_DOWN) + dy += 20; + if (et->priv->scroll_direction & ET_SCROLL_UP) + dy -= 20; + + if (et->priv->scroll_direction & ET_SCROLL_RIGHT) + dx += 20; + if (et->priv->scroll_direction & ET_SCROLL_LEFT) + dx -= 20; + + scrollable = GTK_SCROLLABLE (et->priv->table_canvas); + + adjustment = gtk_scrollable_get_hadjustment (scrollable); + + page_size = gtk_adjustment_get_page_size (adjustment); + lower = gtk_adjustment_get_lower (adjustment); + upper = gtk_adjustment_get_upper (adjustment); + + old_h_value = gtk_adjustment_get_value (adjustment); + new_h_value = CLAMP (old_h_value + dx, lower, upper - page_size); + + gtk_adjustment_set_value (adjustment, new_h_value); + + adjustment = gtk_scrollable_get_vadjustment (scrollable); + + page_size = gtk_adjustment_get_page_size (adjustment); + lower = gtk_adjustment_get_lower (adjustment); + upper = gtk_adjustment_get_upper (adjustment); + + old_v_value = gtk_adjustment_get_value (adjustment); + new_v_value = CLAMP (old_v_value + dy, lower, upper - page_size); + + gtk_adjustment_set_value (adjustment, new_v_value); + + if (new_h_value != old_h_value || new_v_value != old_v_value) + do_drag_motion ( + et, + et->priv->last_drop_context, + et->priv->last_drop_x, + et->priv->last_drop_y, + et->priv->last_drop_time); + + return TRUE; +} + +static void +scroll_on (ETree *et, + guint scroll_direction) +{ + if (et->priv->scroll_idle_id == 0 || + scroll_direction != et->priv->scroll_direction) { + if (et->priv->scroll_idle_id != 0) + g_source_remove (et->priv->scroll_idle_id); + et->priv->scroll_direction = scroll_direction; + et->priv->scroll_idle_id = g_timeout_add (100, scroll_timeout, et); + } +} + +static void +scroll_off (ETree *et) +{ + if (et->priv->scroll_idle_id) { + g_source_remove (et->priv->scroll_idle_id); + et->priv->scroll_idle_id = 0; + } +} + +static gboolean +hover_timeout (gpointer data) +{ + ETree *et = data; + gint x = et->priv->hover_x; + gint y = et->priv->hover_y; + gint row, col; + ETreePath path; + + e_tree_get_cell_at (et, x, y, &row, &col); + + path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + if (path && e_tree_model_node_is_expandable (et->priv->model, path)) { + if (!e_tree_table_adapter_node_is_expanded (et->priv->etta, path)) { + if (e_tree_model_has_save_id (et->priv->model) && + e_tree_model_has_get_node_by_id (et->priv->model)) + et->priv->expanded_list = g_list_prepend ( + et->priv->expanded_list, + e_tree_model_get_save_id ( + et->priv->model, path)); + + e_tree_table_adapter_node_set_expanded ( + et->priv->etta, path, TRUE); + } + } + + return TRUE; +} + +static void +hover_on (ETree *et, + gint x, + gint y) +{ + et->priv->hover_x = x; + et->priv->hover_y = y; + if (et->priv->hover_idle_id != 0) + g_source_remove (et->priv->hover_idle_id); + et->priv->hover_idle_id = g_timeout_add (500, hover_timeout, et); +} + +static void +hover_off (ETree *et) +{ + if (et->priv->hover_idle_id) { + g_source_remove (et->priv->hover_idle_id); + et->priv->hover_idle_id = 0; + } +} + +static void +collapse_drag (ETree *et, + ETreePath drop) +{ + GList *list; + + /* We only want to leave open parents of the node dropped in. + * Not the node itself. */ + if (drop) { + drop = e_tree_model_node_get_parent (et->priv->model, drop); + } + + for (list = et->priv->expanded_list; list; list = list->next) { + gchar *save_id = list->data; + ETreePath path; + + path = e_tree_model_get_node_by_id (et->priv->model, save_id); + if (path) { + ETreePath search; + gboolean found = FALSE; + + for (search = drop; search; + search = e_tree_model_node_get_parent ( + et->priv->model, search)) { + if (path == search) { + found = TRUE; + break; + } + } + + if (!found) + e_tree_table_adapter_node_set_expanded ( + et->priv->etta, path, FALSE); + } + g_free (save_id); + } + g_list_free (et->priv->expanded_list); + et->priv->expanded_list = NULL; +} + +static void +context_destroyed (gpointer data, + GObject *ctx) +{ + ETree *et = data; + if (et->priv) { + et->priv->last_drop_x = 0; + et->priv->last_drop_y = 0; + et->priv->last_drop_time = 0; + et->priv->last_drop_context = NULL; + collapse_drag (et, NULL); + scroll_off (et); + hover_off (et); + } + g_object_unref (et); +} + +static void +context_connect (ETree *et, + GdkDragContext *context) +{ + if (context == et->priv->last_drop_context) + return; + + if (et->priv->last_drop_context) + g_object_weak_unref ( + G_OBJECT (et->priv->last_drop_context), + context_destroyed, et); + else + g_object_ref (et); + + g_object_weak_ref (G_OBJECT (context), context_destroyed, et); +} + +static void +et_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time, + ETree *et) +{ + g_signal_emit ( + et, + et_signals[TREE_DRAG_LEAVE], 0, + et->priv->drop_row, + et->priv->drop_path, + et->priv->drop_col, + context, + time); + et->priv->drop_row = -1; + et->priv->drop_col = -1; + + scroll_off (et); + hover_off (et); +} + +static gboolean +et_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETree *et) +{ + GtkAllocation allocation; + gint ret_val; + guint direction = 0; + + et->priv->last_drop_x = x; + et->priv->last_drop_y = y; + et->priv->last_drop_time = time; + context_connect (et, context); + et->priv->last_drop_context = context; + + if (et->priv->hover_idle_id != 0) { + if (abs (et->priv->hover_x - x) > 3 || + abs (et->priv->hover_y - y) > 3) { + hover_on (et, x, y); + } + } else { + hover_on (et, x, y); + } + + ret_val = do_drag_motion (et, context, x, y, time); + + gtk_widget_get_allocation (widget, &allocation); + + if (y < 20) + direction |= ET_SCROLL_UP; + if (y > allocation.height - 20) + direction |= ET_SCROLL_DOWN; + if (x < 20) + direction |= ET_SCROLL_LEFT; + if (x > allocation.width - 20) + direction |= ET_SCROLL_RIGHT; + + if (direction != 0) + scroll_on (et, direction); + else + scroll_off (et); + + return ret_val; +} + +static gboolean +et_drag_drop (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + ETree *et) +{ + gboolean ret_val = FALSE; + gint row, col; + ETreePath path; + + e_tree_get_cell_at (et, x, y, &row, &col); + + path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + + if (row != et->priv->drop_row && col != et->priv->drop_row) { + g_signal_emit ( + et, et_signals[TREE_DRAG_LEAVE], 0, + et->priv->drop_row, + et->priv->drop_path, + et->priv->drop_col, + context, + time); + g_signal_emit ( + et, et_signals[TREE_DRAG_MOTION], 0, + row, + path, + col, + context, + x, + y, + time, + &ret_val); + } + et->priv->drop_row = row; + et->priv->drop_path = path; + et->priv->drop_col = col; + + g_signal_emit ( + et, et_signals[TREE_DRAG_DROP], 0, + et->priv->drop_row, + et->priv->drop_path, + et->priv->drop_col, + context, + x, + y, + time, + &ret_val); + + et->priv->drop_row = -1; + et->priv->drop_path = NULL; + et->priv->drop_col = -1; + + collapse_drag (et, path); + + scroll_off (et); + return ret_val; +} + +static void +et_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time, + ETree *et) +{ + gint row, col; + ETreePath path; + + e_tree_get_cell_at (et, x, y, &row, &col); + + path = e_tree_table_adapter_node_at_row (et->priv->etta, row); + g_signal_emit ( + et, et_signals[TREE_DRAG_DATA_RECEIVED], 0, + row, + path, + col, + context, + x, + y, + selection_data, + info, + time); +} + +static void +e_tree_class_init (ETreeClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (ETreePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = et_dispose; + object_class->set_property = et_set_property; + object_class->get_property = et_get_property; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->grab_focus = et_grab_focus; + widget_class->unrealize = et_unrealize; + widget_class->style_set = et_canvas_style_set; + widget_class->focus = et_focus; + + class->start_drag = et_real_start_drag; + + et_signals[CURSOR_CHANGE] = g_signal_new ( + "cursor_change", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, cursor_change), + NULL, NULL, + e_marshal_NONE__INT_POINTER, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_POINTER); + + et_signals[CURSOR_ACTIVATED] = g_signal_new ( + "cursor_activated", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, cursor_activated), + NULL, NULL, + e_marshal_NONE__INT_POINTER, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_POINTER); + + et_signals[SELECTION_CHANGE] = g_signal_new ( + "selection_change", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, selection_change), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + et_signals[DOUBLE_CLICK] = g_signal_new ( + "double_click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, double_click), + NULL, NULL, + e_marshal_NONE__INT_POINTER_INT_BOXED, + G_TYPE_NONE, 4, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[RIGHT_CLICK] = g_signal_new ( + "right_click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, right_click), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_POINTER_INT_BOXED, + G_TYPE_BOOLEAN, 4, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[CLICK] = g_signal_new ( + "click", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, click), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_POINTER_INT_BOXED, + G_TYPE_BOOLEAN, 4, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[KEY_PRESS] = g_signal_new ( + "key_press", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, key_press), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__INT_POINTER_INT_BOXED, + G_TYPE_BOOLEAN, 4, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[START_DRAG] = g_signal_new ( + "start_drag", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, start_drag), + NULL, NULL, + e_marshal_NONE__INT_POINTER_INT_BOXED, + G_TYPE_NONE, 4, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[STATE_CHANGE] = g_signal_new ( + "state_change", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, state_change), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + et_signals[WHITE_SPACE_EVENT] = g_signal_new ( + "white_space_event", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, white_space_event), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__POINTER, + G_TYPE_BOOLEAN, 1, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + + et_signals[TREE_DRAG_BEGIN] = g_signal_new ( + "tree_drag_begin", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, tree_drag_begin), + NULL, NULL, + e_marshal_NONE__INT_POINTER_INT_BOXED, + G_TYPE_NONE, 4, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT); + + et_signals[TREE_DRAG_END] = g_signal_new ( + "tree_drag_end", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, tree_drag_end), + NULL, NULL, + e_marshal_NONE__INT_POINTER_INT_BOXED, + G_TYPE_NONE, 4, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT); + + et_signals[TREE_DRAG_DATA_GET] = g_signal_new ( + "tree_drag_data_get", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, tree_drag_data_get), + NULL, NULL, + e_marshal_NONE__INT_POINTER_INT_OBJECT_BOXED_UINT_UINT, + G_TYPE_NONE, 7, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + GTK_TYPE_SELECTION_DATA | G_SIGNAL_TYPE_STATIC_SCOPE, + G_TYPE_UINT, + G_TYPE_UINT); + + et_signals[TREE_DRAG_DATA_DELETE] = g_signal_new ( + "tree_drag_data_delete", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, tree_drag_data_delete), + NULL, NULL, + e_marshal_NONE__INT_POINTER_INT_OBJECT, + G_TYPE_NONE, 4, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT); + + et_signals[TREE_DRAG_LEAVE] = g_signal_new ( + "tree_drag_leave", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, tree_drag_leave), + NULL, NULL, + e_marshal_NONE__INT_POINTER_INT_OBJECT_UINT, + G_TYPE_NONE, 5, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + G_TYPE_UINT); + + et_signals[TREE_DRAG_MOTION] = g_signal_new ( + "tree_drag_motion", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, tree_drag_motion), + NULL, NULL, + e_marshal_BOOLEAN__INT_POINTER_INT_OBJECT_INT_INT_UINT, + G_TYPE_BOOLEAN, 7, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + G_TYPE_INT, + G_TYPE_INT, + G_TYPE_UINT); + + et_signals[TREE_DRAG_DROP] = g_signal_new ( + "tree_drag_drop", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, tree_drag_drop), + NULL, NULL, + e_marshal_BOOLEAN__INT_POINTER_INT_OBJECT_INT_INT_UINT, + G_TYPE_BOOLEAN, 7, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + G_TYPE_INT, + G_TYPE_INT, + G_TYPE_UINT); + + et_signals[TREE_DRAG_DATA_RECEIVED] = g_signal_new ( + "tree_drag_data_received", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ETreeClass, tree_drag_data_received), + NULL, NULL, + e_marshal_NONE__INT_POINTER_INT_OBJECT_INT_INT_BOXED_UINT_UINT, + G_TYPE_NONE, 9, + G_TYPE_INT, + G_TYPE_POINTER, + G_TYPE_INT, + GDK_TYPE_DRAG_CONTEXT, + G_TYPE_INT, + G_TYPE_INT, + GTK_TYPE_SELECTION_DATA, + G_TYPE_UINT, + G_TYPE_UINT); + + g_object_class_install_property ( + object_class, + PROP_LENGTH_THRESHOLD, + g_param_spec_int ( + "length_threshold", + "Length Threshold", + "Length Threshold", + 0, G_MAXINT, 0, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_HORIZONTAL_DRAW_GRID, + g_param_spec_boolean ( + "horizontal_draw_grid", + "Horizontal Draw Grid", + "Horizontal Draw Grid", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_VERTICAL_DRAW_GRID, + g_param_spec_boolean ( + "vertical_draw_grid", + "Vertical Draw Grid", + "Vertical Draw Grid", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_DRAW_FOCUS, + g_param_spec_boolean ( + "drawfocus", + "Draw focus", + "Draw focus", + FALSE, + G_PARAM_WRITABLE)); + + g_object_class_install_property ( + object_class, + PROP_ETTA, + g_param_spec_object ( + "ETreeTableAdapter", + "ETree table adapter", + "ETree table adapter", + E_TYPE_TREE_TABLE_ADAPTER, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_UNIFORM_ROW_HEIGHT, + g_param_spec_boolean ( + "uniform_row_height", + "Uniform row height", + "Uniform row height", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_ALWAYS_SEARCH, + g_param_spec_boolean ( + "always_search", + "Always search", + "Always search", + FALSE, + G_PARAM_READWRITE)); + + gtk_widget_class_install_style_property ( + widget_class, + g_param_spec_int ( + "expander_size", + "Expander Size", + "Size of the expander arrow", + 0, G_MAXINT, 10, + G_PARAM_READABLE)); + + gtk_widget_class_install_style_property ( + widget_class, + g_param_spec_int ( + "vertical-spacing", + "Vertical Row Spacing", + "Vertical space between rows. " + "It is added to top and to bottom of a row", + 0, G_MAXINT, 3, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /* Scrollable interface */ + g_object_class_override_property ( + object_class, PROP_HADJUSTMENT, "hadjustment"); + g_object_class_override_property ( + object_class, PROP_VADJUSTMENT, "vadjustment"); + g_object_class_override_property ( + object_class, PROP_HSCROLL_POLICY, "hscroll-policy"); + g_object_class_override_property ( + object_class, PROP_VSCROLL_POLICY, "vscroll-policy"); + + gal_a11y_e_tree_init (); +} + +static void +tree_size_allocate (GtkWidget *widget, + GtkAllocation *alloc, + ETree *tree) +{ + gdouble width; + + g_return_if_fail (tree != NULL); + g_return_if_fail (tree->priv != NULL); + g_return_if_fail (tree->priv->info_text != NULL); + + gnome_canvas_get_scroll_region ( + GNOME_CANVAS (tree->priv->table_canvas), + NULL, NULL, &width, NULL); + + width -= 60.0; + + g_object_set ( + tree->priv->info_text, "width", width, + "clip_width", width, NULL); +} + +/** + * e_tree_set_info_message: + * @tree: #ETree instance + * @info_message: Message to set. Can be NULL. + * + * Creates an info message in table area, or removes old. + **/ +void +e_tree_set_info_message (ETree *tree, + const gchar *info_message) +{ + GtkAllocation allocation; + GtkWidget *widget; + + g_return_if_fail (tree != NULL); + g_return_if_fail (tree->priv != NULL); + + if (!tree->priv->info_text && (!info_message || !*info_message)) + return; + + if (!info_message || !*info_message) { + g_signal_handler_disconnect (tree, tree->priv->info_text_resize_id); + g_object_run_dispose (G_OBJECT (tree->priv->info_text)); + tree->priv->info_text = NULL; + return; + } + + widget = GTK_WIDGET (tree->priv->table_canvas); + gtk_widget_get_allocation (widget, &allocation); + + if (!tree->priv->info_text) { + if (allocation.width > 60) { + tree->priv->info_text = gnome_canvas_item_new ( + GNOME_CANVAS_GROUP (gnome_canvas_root (tree->priv->table_canvas)), + e_text_get_type (), + "line_wrap", TRUE, + "clip", TRUE, + "justification", GTK_JUSTIFY_LEFT, + "text", info_message, + "width", (gdouble) allocation.width - 60.0, + "clip_width", (gdouble) allocation.width - 60.0, + NULL); + + e_canvas_item_move_absolute (tree->priv->info_text, 30, 30); + + tree->priv->info_text_resize_id = g_signal_connect ( + tree, "size_allocate", + G_CALLBACK (tree_size_allocate), tree); + } + } else + gnome_canvas_item_set (tree->priv->info_text, "text", info_message, NULL); +} + +void +e_tree_freeze_state_change (ETree *tree) +{ + g_return_if_fail (tree != NULL); + + tree->priv->state_change_freeze++; + if (tree->priv->state_change_freeze == 1) + tree->priv->state_changed = FALSE; + + g_return_if_fail (tree->priv->state_change_freeze != 0); +} + +void +e_tree_thaw_state_change (ETree *tree) +{ + g_return_if_fail (tree != NULL); + g_return_if_fail (tree->priv->state_change_freeze != 0); + + tree->priv->state_change_freeze--; + if (tree->priv->state_change_freeze == 0 && tree->priv->state_changed) { + tree->priv->state_changed = FALSE; + e_tree_state_change (tree); + } +} diff --git a/e-util/e-tree.h b/e-util/e-tree.h new file mode 100644 index 0000000000..1d6243cc61 --- /dev/null +++ b/e-util/e-tree.h @@ -0,0 +1,376 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_TREE_H_ +#define _E_TREE_H_ + +#include <gtk/gtk.h> +#include <libxml/tree.h> +#include <libgnomecanvas/libgnomecanvas.h> + +#include <e-util/e-printable.h> +#include <e-util/e-table-extras.h> +#include <e-util/e-table-item.h> +#include <e-util/e-table-specification.h> +#include <e-util/e-table-state.h> +#include <e-util/e-tree-model.h> +#include <e-util/e-tree-table-adapter.h> + +#define E_TREE_USE_TREE_SELECTION + +#ifdef E_TREE_USE_TREE_SELECTION +#include <e-util/e-tree-selection-model.h> +#endif + +/* Standard GObject macros */ +#define E_TYPE_TREE \ + (e_tree_get_type ()) +#define E_TREE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_TREE, ETree)) +#define E_TREE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_TREE, ETreeClass)) +#define E_IS_TREE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_TREE)) +#define E_IS_TREE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_TREE)) +#define E_TREE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_TREE)) + +G_BEGIN_DECLS + +typedef struct _ETreeDragSourceSite ETreeDragSourceSite; + +typedef struct _ETree ETree; +typedef struct _ETreeClass ETreeClass; +typedef struct _ETreePrivate ETreePrivate; + +struct _ETree { + GtkTable parent; + ETreePrivate *priv; +}; + +struct _ETreeClass { + GtkTableClass parent_class; + + void (*cursor_change) (ETree *et, + gint row, + ETreePath path); + void (*cursor_activated) (ETree *et, + gint row, + ETreePath path); + void (*selection_change) (ETree *et); + void (*double_click) (ETree *et, + gint row, + ETreePath path, + gint col, + GdkEvent *event); + gboolean (*right_click) (ETree *et, + gint row, + ETreePath path, + gint col, + GdkEvent *event); + gboolean (*click) (ETree *et, + gint row, + ETreePath path, + gint col, + GdkEvent *event); + gboolean (*key_press) (ETree *et, + gint row, + ETreePath path, + gint col, + GdkEvent *event); + gboolean (*start_drag) (ETree *et, + gint row, + ETreePath path, + gint col, + GdkEvent *event); + void (*state_change) (ETree *et); + gboolean (*white_space_event) (ETree *et, + GdkEvent *event); + + /* Source side drag signals */ + void (*tree_drag_begin) (ETree *tree, + gint row, + ETreePath path, + gint col, + GdkDragContext *context); + void (*tree_drag_end) (ETree *tree, + gint row, + ETreePath path, + gint col, + GdkDragContext *context); + void (*tree_drag_data_get) (ETree *tree, + gint row, + ETreePath path, + gint col, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time); + void (*tree_drag_data_delete) + (ETree *tree, + gint row, + ETreePath path, + gint col, + GdkDragContext *context); + + /* Target side drag signals */ + void (*tree_drag_leave) (ETree *tree, + gint row, + ETreePath path, + gint col, + GdkDragContext *context, + guint time); + gboolean (*tree_drag_motion) (ETree *tree, + gint row, + ETreePath path, + gint col, + GdkDragContext *context, + gint x, + gint y, + guint time); + gboolean (*tree_drag_drop) (ETree *tree, + gint row, + ETreePath path, + gint col, + GdkDragContext *context, + gint x, + gint y, + guint time); + void (*tree_drag_data_received) + (ETree *tree, + gint row, + ETreePath path, + gint col, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time); +}; + +GType e_tree_get_type (void) G_GNUC_CONST; +gboolean e_tree_construct (ETree *e_tree, + ETreeModel *etm, + ETableExtras *ete, + const gchar *spec, + const gchar *state); +GtkWidget * e_tree_new (ETreeModel *etm, + ETableExtras *ete, + const gchar *spec, + const gchar *state); + +/* Create an ETree using files. */ +gboolean e_tree_construct_from_spec_file (ETree *e_tree, + ETreeModel *etm, + ETableExtras *ete, + const gchar *spec_fn, + const gchar *state_fn); +GtkWidget * e_tree_new_from_spec_file (ETreeModel *etm, + ETableExtras *ete, + const gchar *spec_fn, + const gchar *state_fn); + +/* To save the state */ +gchar * e_tree_get_state (ETree *e_tree); +void e_tree_save_state (ETree *e_tree, + const gchar *filename); +ETableState * e_tree_get_state_object (ETree *e_tree); +ETableSpecification * + e_tree_get_spec (ETree *e_tree); + +/* note that it is more efficient to provide the state at creation time */ +void e_tree_set_search_column (ETree *e_tree, + gint col); +void e_tree_set_state (ETree *e_tree, + const gchar *state); +void e_tree_set_state_object (ETree *e_tree, + ETableState *state); +void e_tree_load_state (ETree *e_tree, + const gchar *filename); +void e_tree_show_cursor_after_reflow (ETree *e_tree); + +void e_tree_set_cursor (ETree *e_tree, + ETreePath path); + +/* NULL means we don't have the cursor. */ +ETreePath e_tree_get_cursor (ETree *e_tree); +void e_tree_selected_row_foreach (ETree *e_tree, + EForeachFunc callback, + gpointer closure); +#ifdef E_TREE_USE_TREE_SELECTION +void e_tree_selected_path_foreach (ETree *e_tree, + ETreeForeachFunc callback, + gpointer closure); +void e_tree_path_foreach (ETree *e_tree, + ETreeForeachFunc callback, + gpointer closure); +#endif +EPrintable *e_tree_get_printable (ETree *e_tree); +gint e_tree_get_next_row (ETree *e_tree, + gint model_row); +gint e_tree_get_prev_row (ETree *e_tree, + gint model_row); +gint e_tree_model_to_view_row (ETree *e_tree, + gint model_row); +gint e_tree_view_to_model_row (ETree *e_tree, + gint view_row); +void e_tree_get_cell_at (ETree *tree, + gint x, + gint y, + gint *row_return, + gint *col_return); +void e_tree_get_cell_geometry (ETree *tree, + gint row, + gint col, + gint *x_return, + gint *y_return, + gint *width_return, + gint *height_return); + +/* Useful accessors */ +ETreeModel * e_tree_get_model (ETree *et); +ESelectionModel * + e_tree_get_selection_model (ETree *et); +ETreeTableAdapter * + e_tree_get_table_adapter (ETree *et); + +/* Drag & drop stuff. */ +/* Target */ +void e_tree_drag_get_data (ETree *tree, + gint row, + gint col, + GdkDragContext *context, + GdkAtom target, + guint32 time); +void e_tree_drag_highlight (ETree *tree, + gint row, + gint col); /* col == -1 to highlight entire row. */ +void e_tree_drag_unhighlight (ETree *tree); +void e_tree_drag_dest_set (ETree *tree, + GtkDestDefaults flags, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions); +void e_tree_drag_dest_set_proxy (ETree *tree, + GdkWindow *proxy_window, + GdkDragProtocol protocol, + gboolean use_coordinates); + +/* There probably should be functions for setting the targets + * as a GtkTargetList + */ +void e_tree_drag_dest_unset (GtkWidget *widget); + +/* Source side */ +void e_tree_drag_source_set (ETree *tree, + GdkModifierType start_button_mask, + const GtkTargetEntry *targets, + gint n_targets, + GdkDragAction actions); +void e_tree_drag_source_unset (ETree *tree); + +/* There probably should be functions for setting the targets + * as a GtkTargetList + */ +GdkDragContext *e_tree_drag_begin (ETree *tree, + gint row, + gint col, + GtkTargetList *targets, + GdkDragAction actions, + gint button, + GdkEvent *event); + +gboolean e_tree_is_dragging (ETree *tree); + +/* Adapter functions */ +gboolean e_tree_node_is_expanded (ETree *et, + ETreePath path); +void e_tree_node_set_expanded (ETree *et, + ETreePath path, + gboolean expanded); +void e_tree_node_set_expanded_recurse + (ETree *et, + ETreePath path, + gboolean expanded); +void e_tree_root_node_set_visible (ETree *et, + gboolean visible); +ETreePath e_tree_node_at_row (ETree *et, + gint row); +gint e_tree_row_of_node (ETree *et, + ETreePath path); +gboolean e_tree_root_node_is_visible (ETree *et); +void e_tree_show_node (ETree *et, + ETreePath path); +void e_tree_save_expanded_state (ETree *et, + gchar *filename); +void e_tree_load_expanded_state (ETree *et, + gchar *filename); + +xmlDoc * e_tree_save_expanded_state_xml (ETree *et); +void e_tree_load_expanded_state_xml (ETree *et, + xmlDoc *doc); + +gint e_tree_row_count (ETree *et); +GtkWidget * e_tree_get_tooltip (ETree *et); +void e_tree_force_expanded_state (ETree *et, + gint state); + +typedef enum { + E_TREE_FIND_NEXT_BACKWARD = 0, + E_TREE_FIND_NEXT_FORWARD = 1 << 0, + E_TREE_FIND_NEXT_WRAP = 1 << 1 +} ETreeFindNextParams; + +gboolean e_tree_find_next (ETree *et, + ETreeFindNextParams params, + ETreePathFunc func, + gpointer data); + +/* This function is only needed in single_selection_mode. */ +void e_tree_right_click_up (ETree *et); + +ETableItem * e_tree_get_item (ETree *et); + +GnomeCanvasItem * + e_tree_get_header_item (ETree *et); + +void e_tree_set_info_message (ETree *tree, + const gchar *info_message); + +void e_tree_freeze_state_change (ETree *table); +void e_tree_thaw_state_change (ETree *table); + +G_END_DECLS + +#endif /* _E_TREE_H_ */ + diff --git a/e-util/e-ui-manager.c b/e-util/e-ui-manager.c index 3308b500d2..e494d351a6 100644 --- a/e-util/e-ui-manager.c +++ b/e-util/e-ui-manager.c @@ -19,7 +19,7 @@ /** * SECTION: e-ui-manager * @short_description: construct menus and toolbars from a UI definition - * @include: e-util/e-ui-manager.h + * @include: e-util/e-util.h * * This is a #GtkUIManager with support for Evolution's "express" mode, * which influences the parsing of UI definitions. diff --git a/e-util/e-ui-manager.h b/e-util/e-ui-manager.h index 9b1f389d76..4c295888d0 100644 --- a/e-util/e-ui-manager.h +++ b/e-util/e-ui-manager.h @@ -16,6 +16,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_UI_MANAGER_H #define E_UI_MANAGER_H diff --git a/e-util/e-unicode.h b/e-util/e-unicode.h index c519c2e6e2..2901744f8b 100644 --- a/e-util/e-unicode.h +++ b/e-util/e-unicode.h @@ -22,6 +22,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef _E_UNICODE_H_ #define _E_UNICODE_H_ diff --git a/e-util/e-url-entry.c b/e-util/e-url-entry.c new file mode 100644 index 0000000000..7752732ccc --- /dev/null +++ b/e-util/e-url-entry.c @@ -0,0 +1,159 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * JP Rosevear <jpr@novell.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-url-entry.h" + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "e-misc-utils.h" + +#define E_URL_ENTRY_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_URL_ENTRY, EUrlEntryPrivate)) + +struct _EUrlEntryPrivate { + GtkWidget *entry; + GtkWidget *button; +}; + +static void button_clicked_cb (GtkWidget *widget, gpointer data); +static void entry_changed_cb (GtkEditable *editable, gpointer data); + +static gboolean mnemonic_activate (GtkWidget *widget, gboolean group_cycling); + +G_DEFINE_TYPE ( + EUrlEntry, + e_url_entry, + GTK_TYPE_HBOX) + +static void +e_url_entry_class_init (EUrlEntryClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EUrlEntryPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->mnemonic_activate = mnemonic_activate; +} + +static void +e_url_entry_init (EUrlEntry *url_entry) +{ + GtkWidget *pixmap; + + url_entry->priv = E_URL_ENTRY_GET_PRIVATE (url_entry); + + url_entry->priv->entry = gtk_entry_new (); + gtk_box_pack_start ( + GTK_BOX (url_entry), url_entry->priv->entry, TRUE, TRUE, 0); + url_entry->priv->button = gtk_button_new (); + gtk_widget_set_sensitive (url_entry->priv->button, FALSE); + gtk_box_pack_start ( + GTK_BOX (url_entry), url_entry->priv->button, FALSE, FALSE, 0); + atk_object_set_name ( + gtk_widget_get_accessible (url_entry->priv->button), + _("Click here to go to URL")); + pixmap = gtk_image_new_from_icon_name ("go-jump", GTK_ICON_SIZE_BUTTON); + gtk_container_add (GTK_CONTAINER (url_entry->priv->button), pixmap); + gtk_widget_show (pixmap); + + gtk_widget_show (url_entry->priv->button); + gtk_widget_show (url_entry->priv->entry); + + g_signal_connect ( + url_entry->priv->button, "clicked", + G_CALLBACK (button_clicked_cb), url_entry); + g_signal_connect ( + url_entry->priv->entry, "changed", + G_CALLBACK (entry_changed_cb), url_entry); +} + +/* GtkWidget::mnemonic_activate() handler for the EUrlEntry */ +static gboolean +mnemonic_activate (GtkWidget *widget, + gboolean group_cycling) +{ + EUrlEntry *url_entry; + EUrlEntryPrivate *priv; + + url_entry = E_URL_ENTRY (widget); + priv = url_entry->priv; + + return gtk_widget_mnemonic_activate (priv->entry, group_cycling); +} + +GtkWidget * +e_url_entry_new (void) +{ + return g_object_new (E_TYPE_URL_ENTRY, NULL); +} + +GtkWidget * +e_url_entry_get_entry (EUrlEntry *url_entry) +{ + EUrlEntryPrivate *priv; + + g_return_val_if_fail (url_entry != NULL, NULL); + g_return_val_if_fail (E_IS_URL_ENTRY (url_entry), NULL); + + priv = url_entry->priv; + + return priv->entry; +} + +static void +button_clicked_cb (GtkWidget *widget, + gpointer data) +{ + EUrlEntry *url_entry; + EUrlEntryPrivate *priv; + const gchar *uri; + + url_entry = E_URL_ENTRY (data); + priv = url_entry->priv; + + uri = gtk_entry_get_text (GTK_ENTRY (priv->entry)); + + /* FIXME Pass a parent window. */ + e_show_uri (NULL, uri); +} + +static void +entry_changed_cb (GtkEditable *editable, + gpointer data) +{ + EUrlEntry *url_entry; + EUrlEntryPrivate *priv; + const gchar *url; + + url_entry = E_URL_ENTRY (data); + priv = url_entry->priv; + + url = gtk_entry_get_text (GTK_ENTRY (priv->entry)); + gtk_widget_set_sensitive (priv->button, url != NULL && *url != '\0'); +} diff --git a/e-util/e-url-entry.h b/e-util/e-url-entry.h new file mode 100644 index 0000000000..0925287b63 --- /dev/null +++ b/e-util/e-url-entry.h @@ -0,0 +1,60 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * JP Rosevear <jpr@novell.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _E_URL_ENTRY_H_ +#define _E_URL_ENTRY_H_ + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define E_TYPE_URL_ENTRY (e_url_entry_get_type ()) +#define E_URL_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_TYPE_URL_ENTRY, EUrlEntry)) +#define E_URL_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), E_TYPE_URL_ENTRY, EUrlEntryClass)) +#define E_IS_URL_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_URL_ENTRY)) +#define E_IS_URL_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), E_TYPE_URL_ENTRY)) + +typedef struct _EUrlEntry EUrlEntry; +typedef struct _EUrlEntryPrivate EUrlEntryPrivate; +typedef struct _EUrlEntryClass EUrlEntryClass; + +struct _EUrlEntry { + GtkBox parent; + + EUrlEntryPrivate *priv; +}; + +struct _EUrlEntryClass { + GtkBoxClass parent_class; +}; + +GType e_url_entry_get_type (void); +GtkWidget *e_url_entry_new (void); +GtkWidget *e_url_entry_get_entry (EUrlEntry *url_entry); + +G_END_DECLS + +#endif /* _E_URL_ENTRY_H_ */ diff --git a/e-util/e-util-enums.h b/e-util/e-util-enums.h index 5f5ef75587..b2da504fb2 100644 --- a/e-util/e-util-enums.h +++ b/e-util/e-util-enums.h @@ -16,6 +16,10 @@ * */ +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + #ifndef E_UTIL_ENUMS_H #define E_UTIL_ENUMS_H diff --git a/e-util/e-util.h b/e-util/e-util.h index fa98153223..a5ab42bd3b 100644 --- a/e-util/e-util.h +++ b/e-util/e-util.h @@ -1,4 +1,6 @@ /* + * e-util.h + * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either @@ -12,145 +14,229 @@ * You should have received a copy of the GNU Lesser General Public * License along with the program; if not, see <http://www.gnu.org/licenses/> * - * - * Authors: - * Chris Lahey <clahey@ximian.com> - * - * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) - * */ #ifndef E_UTIL_H #define E_UTIL_H -#include <sys/types.h> -#include <gtk/gtk.h> -#include <limits.h> +#define __E_UTIL_H_INSIDE__ #include <libedataserver/libedataserver.h> -#include <libevolution-utils/evolution-util.h> - -#include <e-util/e-marshal.h> +#include <e-util/e-action-combo-box.h> +#include <e-util/e-activity-bar.h> +#include <e-util/e-activity-proxy.h> +#include <e-util/e-activity.h> +#include <e-util/e-alarm-selector.h> +#include <e-util/e-alert-bar.h> +#include <e-util/e-alert-dialog.h> +#include <e-util/e-alert-sink.h> +#include <e-util/e-alert.h> +#include <e-util/e-attachment-bar.h> +#include <e-util/e-attachment-button.h> +#include <e-util/e-attachment-dialog.h> +#include <e-util/e-attachment-handler-image.h> +#include <e-util/e-attachment-handler-sendto.h> +#include <e-util/e-attachment-handler.h> +#include <e-util/e-attachment-icon-view.h> +#include <e-util/e-attachment-paned.h> +#include <e-util/e-attachment-store.h> +#include <e-util/e-attachment-tree-view.h> +#include <e-util/e-attachment-view.h> +#include <e-util/e-attachment.h> +#include <e-util/e-auth-combo-box.h> +#include <e-util/e-autocomplete-selector.h> +#include <e-util/e-bit-array.h> +#include <e-util/e-book-source-config.h> +#include <e-util/e-buffer-tagger.h> +#include <e-util/e-cal-source-config.h> +#include <e-util/e-calendar-item.h> +#include <e-util/e-calendar.h> +#include <e-util/e-canvas-background.h> +#include <e-util/e-canvas-utils.h> +#include <e-util/e-canvas-vbox.h> +#include <e-util/e-canvas.h> +#include <e-util/e-categories-config.h> +#include <e-util/e-categories-dialog.h> +#include <e-util/e-categories-editor.h> +#include <e-util/e-categories-selector.h> +#include <e-util/e-category-completion.h> +#include <e-util/e-category-editor.h> +#include <e-util/e-cell-checkbox.h> +#include <e-util/e-cell-combo.h> +#include <e-util/e-cell-date-edit.h> +#include <e-util/e-cell-date.h> +#include <e-util/e-cell-hbox.h> +#include <e-util/e-cell-number.h> +#include <e-util/e-cell-percent.h> +#include <e-util/e-cell-pixbuf.h> +#include <e-util/e-cell-popup.h> +#include <e-util/e-cell-renderer-color.h> +#include <e-util/e-cell-size.h> +#include <e-util/e-cell-text.h> +#include <e-util/e-cell-toggle.h> +#include <e-util/e-cell-tree.h> +#include <e-util/e-cell-vbox.h> +#include <e-util/e-cell.h> +#include <e-util/e-charset-combo-box.h> +#include <e-util/e-charset.h> +#include <e-util/e-client-utils.h> +#include <e-util/e-config.h> +#include <e-util/e-contact-map-window.h> +#include <e-util/e-contact-map.h> +#include <e-util/e-contact-marker.h> +#include <e-util/e-contact-store.h> +#include <e-util/e-dateedit.h> +#include <e-util/e-datetime-format.h> +#include <e-util/e-destination-store.h> +#include <e-util/e-dialog-utils.h> +#include <e-util/e-dialog-widgets.h> +#include <e-util/e-event.h> +#include <e-util/e-file-request.h> +#include <e-util/e-file-utils.h> +#include <e-util/e-filter-code.h> +#include <e-util/e-filter-color.h> +#include <e-util/e-filter-datespec.h> +#include <e-util/e-filter-element.h> +#include <e-util/e-filter-file.h> +#include <e-util/e-filter-input.h> +#include <e-util/e-filter-int.h> +#include <e-util/e-filter-option.h> +#include <e-util/e-filter-part.h> +#include <e-util/e-filter-rule.h> +#include <e-util/e-focus-tracker.h> +#include <e-util/e-html-utils.h> +#include <e-util/e-icon-factory.h> +#include <e-util/e-image-chooser.h> +#include <e-util/e-import-assistant.h> +#include <e-util/e-import.h> +#include <e-util/e-interval-chooser.h> +#include <e-util/e-mail-identity-combo-box.h> +#include <e-util/e-mail-signature-combo-box.h> +#include <e-util/e-mail-signature-editor.h> +#include <e-util/e-mail-signature-manager.h> +#include <e-util/e-mail-signature-preview.h> +#include <e-util/e-mail-signature-script-dialog.h> +#include <e-util/e-mail-signature-tree-view.h> +#include <e-util/e-map.h> +#include <e-util/e-menu-tool-action.h> +#include <e-util/e-menu-tool-button.h> +#include <e-util/e-misc-utils.h> +#include <e-util/e-mktemp.h> +#include <e-util/e-name-selector-dialog.h> +#include <e-util/e-name-selector-entry.h> +#include <e-util/e-name-selector-list.h> +#include <e-util/e-name-selector-model.h> +#include <e-util/e-name-selector.h> +#include <e-util/e-online-button.h> +#include <e-util/e-paned.h> +#include <e-util/e-passwords.h> +#include <e-util/e-picture-gallery.h> +#include <e-util/e-plugin-ui.h> +#include <e-util/e-plugin.h> +#include <e-util/e-poolv.h> +#include <e-util/e-popup-action.h> +#include <e-util/e-popup-menu.h> +#include <e-util/e-port-entry.h> +#include <e-util/e-preferences-window.h> +#include <e-util/e-preview-pane.h> +#include <e-util/e-print.h> +#include <e-util/e-printable.h> +#include <e-util/e-reflow-model.h> +#include <e-util/e-reflow.h> +#include <e-util/e-rule-context.h> +#include <e-util/e-rule-editor.h> +#include <e-util/e-search-bar.h> +#include <e-util/e-selectable.h> +#include <e-util/e-selection-model-array.h> +#include <e-util/e-selection-model-simple.h> +#include <e-util/e-selection-model.h> +#include <e-util/e-selection.h> +#include <e-util/e-send-options.h> +#include <e-util/e-sorter-array.h> +#include <e-util/e-sorter.h> +#include <e-util/e-source-combo-box.h> +#include <e-util/e-source-config-backend.h> +#include <e-util/e-source-config-dialog.h> +#include <e-util/e-source-config.h> +#include <e-util/e-source-selector-dialog.h> +#include <e-util/e-source-selector.h> +#include <e-util/e-source-util.h> +#include <e-util/e-spell-entry.h> +#include <e-util/e-stock-request.h> +#include <e-util/e-table-click-to-add.h> +#include <e-util/e-table-col-dnd.h> +#include <e-util/e-table-col.h> +#include <e-util/e-table-column-specification.h> +#include <e-util/e-table-config.h> +#include <e-util/e-table-defines.h> +#include <e-util/e-table-extras.h> +#include <e-util/e-table-field-chooser-dialog.h> +#include <e-util/e-table-field-chooser-item.h> +#include <e-util/e-table-field-chooser.h> +#include <e-util/e-table-group-container.h> +#include <e-util/e-table-group-leaf.h> +#include <e-util/e-table-group.h> +#include <e-util/e-table-header-item.h> +#include <e-util/e-table-header-utils.h> +#include <e-util/e-table-header.h> +#include <e-util/e-table-item.h> +#include <e-util/e-table-memory-callbacks.h> +#include <e-util/e-table-memory-store.h> +#include <e-util/e-table-memory.h> +#include <e-util/e-table-model.h> +#include <e-util/e-table-one.h> +#include <e-util/e-table-search.h> +#include <e-util/e-table-selection-model.h> +#include <e-util/e-table-sort-info.h> +#include <e-util/e-table-sorted-variable.h> +#include <e-util/e-table-sorted.h> +#include <e-util/e-table-sorter.h> +#include <e-util/e-table-sorting-utils.h> +#include <e-util/e-table-specification.h> +#include <e-util/e-table-state.h> +#include <e-util/e-table-subset-variable.h> +#include <e-util/e-table-subset.h> +#include <e-util/e-table-utils.h> +#include <e-util/e-table-without.h> +#include <e-util/e-table.h> +#include <e-util/e-text-event-processor-emacs-like.h> +#include <e-util/e-text-event-processor-types.h> +#include <e-util/e-text-event-processor.h> +#include <e-util/e-text-model-repos.h> +#include <e-util/e-text-model.h> +#include <e-util/e-text.h> +#include <e-util/e-timezone-dialog.h> +#include <e-util/e-tree-memory-callbacks.h> +#include <e-util/e-tree-memory.h> +#include <e-util/e-tree-model-generator.h> +#include <e-util/e-tree-model.h> +#include <e-util/e-tree-selection-model.h> +#include <e-util/e-tree-sorted.h> +#include <e-util/e-tree-table-adapter.h> +#include <e-util/e-tree.h> +#include <e-util/e-ui-manager.h> +#include <e-util/e-unicode.h> +#include <e-util/e-url-entry.h> #include <e-util/e-util-enums.h> - -G_BEGIN_DECLS - -typedef enum { - E_FOCUS_NONE, - E_FOCUS_CURRENT, - E_FOCUS_START, - E_FOCUS_END -} EFocus; - -typedef enum { - E_RESTORE_WINDOW_SIZE = 1 << 0, - E_RESTORE_WINDOW_POSITION = 1 << 1 -} ERestoreWindowFlags; - -const gchar * e_get_accels_filename (void); -void e_show_uri (GtkWindow *parent, - const gchar *uri); -void e_display_help (GtkWindow *parent, - const gchar *link_id); -void e_restore_window (GtkWindow *window, - const gchar *settings_path, - ERestoreWindowFlags flags); -GtkAction * e_lookup_action (GtkUIManager *ui_manager, - const gchar *action_name); -GtkActionGroup *e_lookup_action_group (GtkUIManager *ui_manager, - const gchar *group_name); -gint e_action_compare_by_label (GtkAction *action1, - GtkAction *action2); -void e_action_group_remove_all_actions - (GtkActionGroup *action_group); -GtkRadioAction *e_radio_action_get_current_action - (GtkRadioAction *radio_action); -void e_action_group_add_actions_localized - (GtkActionGroup *action_group, - const gchar *translation_domain, - const GtkActionEntry *entries, - guint n_entries, - gpointer user_data); -void e_categories_add_change_hook (GHookFunc func, - gpointer object); - -gchar * e_str_without_underscores (const gchar *string); -gint e_str_compare (gconstpointer x, - gconstpointer y); -gint e_str_case_compare (gconstpointer x, - gconstpointer y); -gint e_collate_compare (gconstpointer x, - gconstpointer y); -gint e_int_compare (gconstpointer x, - gconstpointer y); -guint32 e_color_to_value (GdkColor *color); - -guint32 e_rgba_to_value (GdkRGBA *rgba); - -/* This only makes a filename safe for usage as a filename. - * It still may have shell meta-characters in it. */ -gchar * e_format_number (gint number); - -typedef gint (*ESortCompareFunc) (gconstpointer first, - gconstpointer second, - gpointer closure); - -void e_bsearch (gconstpointer key, - gconstpointer base, - gsize nmemb, - gsize size, - ESortCompareFunc compare, - gpointer closure, - gsize *start, - gsize *end); - -gsize e_strftime_fix_am_pm (gchar *str, - gsize max, - const gchar *fmt, - const struct tm *tm); -gsize e_utf8_strftime_fix_am_pm (gchar *str, - gsize max, - const gchar *fmt, - const struct tm *tm); -const gchar * e_get_month_name (GDateMonth month, - gboolean abbreviated); -const gchar * e_get_weekday_name (GDateWeekday weekday, - gboolean abbreviated); - -gboolean e_file_lock_create (void); -void e_file_lock_destroy (void); -gboolean e_file_lock_exists (void); - -gchar * e_util_guess_mime_type (const gchar *filename, - gboolean localfile); - -GSList * e_util_get_category_filter_options - (void); -GList * e_util_get_searchable_categories (void); - -/* Useful GBinding transform functions */ -gboolean e_binding_transform_color_to_string - (GBinding *binding, - const GValue *source_value, - GValue *target_value, - gpointer not_used); -gboolean e_binding_transform_string_to_color - (GBinding *binding, - const GValue *source_value, - GValue *target_value, - gpointer not_used); -gboolean e_binding_transform_source_to_uid - (GBinding *binding, - const GValue *source_value, - GValue *target_value, - ESourceRegistry *registry); -gboolean e_binding_transform_uid_to_source - (GBinding *binding, - const GValue *source_value, - GValue *target_value, - ESourceRegistry *registry); - -G_END_DECLS +#include <e-util/e-web-view-gtkhtml.h> +#include <e-util/e-web-view-preview.h> +#include <e-util/e-web-view.h> +#include <e-util/e-xml-utils.h> +#include <e-util/ea-cell-table.h> +#include <e-util/ea-factory.h> +#include <e-util/gal-define-views-dialog.h> +#include <e-util/gal-define-views-model.h> +#include <e-util/gal-view-collection.h> +#include <e-util/gal-view-etable.h> +#include <e-util/gal-view-factory-etable.h> +#include <e-util/gal-view-factory.h> +#include <e-util/gal-view-instance-save-as-dialog.h> +#include <e-util/gal-view-instance.h> +#include <e-util/gal-view-new-dialog.h> +#include <e-util/gal-view.h> + +#undef __E_UTIL_H_INSIDE__ #endif /* E_UTIL_H */ + diff --git a/e-util/e-web-view-gtkhtml.c b/e-util/e-web-view-gtkhtml.c new file mode 100644 index 0000000000..7303277f6a --- /dev/null +++ b/e-util/e-web-view-gtkhtml.c @@ -0,0 +1,2317 @@ +/* + * e-web-view-gtkhtml.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-web-view-gtkhtml.h" + +#include <string.h> +#include <glib/gi18n-lib.h> + +#include <camel/camel.h> +#include <libebackend/libebackend.h> + +#include "e-alert-dialog.h" +#include "e-alert-sink.h" +#include "e-misc-utils.h" +#include "e-plugin-ui.h" +#include "e-popup-action.h" +#include "e-selectable.h" + +#define E_WEB_VIEW_GTKHTML_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_WEB_VIEW_GTKHTML, EWebViewGtkHTMLPrivate)) + +typedef struct _EWebViewGtkHTMLRequest EWebViewGtkHTMLRequest; + +struct _EWebViewGtkHTMLPrivate { + GList *requests; + GtkUIManager *ui_manager; + gchar *selected_uri; + GdkPixbufAnimation *cursor_image; + + GtkAction *open_proxy; + GtkAction *print_proxy; + GtkAction *save_as_proxy; + + GtkTargetList *copy_target_list; + GtkTargetList *paste_target_list; + + /* Lockdown Options */ + guint disable_printing : 1; + guint disable_save_to_disk : 1; +}; + +struct _EWebViewGtkHTMLRequest { + GFile *file; + EWebViewGtkHTML *web_view; + GCancellable *cancellable; + GInputStream *input_stream; + GtkHTMLStream *output_stream; + gchar buffer[4096]; +}; + +enum { + PROP_0, + PROP_ANIMATE, + PROP_CARET_MODE, + PROP_COPY_TARGET_LIST, + PROP_DISABLE_PRINTING, + PROP_DISABLE_SAVE_TO_DISK, + PROP_EDITABLE, + PROP_INLINE_SPELLING, + PROP_MAGIC_LINKS, + PROP_MAGIC_SMILEYS, + PROP_OPEN_PROXY, + PROP_PASTE_TARGET_LIST, + PROP_PRINT_PROXY, + PROP_SAVE_AS_PROXY, + PROP_SELECTED_URI, + PROP_CURSOR_IMAGE +}; + +enum { + COPY_CLIPBOARD, + CUT_CLIPBOARD, + PASTE_CLIPBOARD, + POPUP_EVENT, + STATUS_MESSAGE, + STOP_LOADING, + UPDATE_ACTIONS, + PROCESS_MAILTO, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +static const gchar *ui = +"<ui>" +" <popup name='context'>" +" <menuitem action='copy-clipboard'/>" +" <separator/>" +" <placeholder name='custom-actions-1'>" +" <menuitem action='open'/>" +" <menuitem action='save-as'/>" +" <menuitem action='http-open'/>" +" <menuitem action='send-message'/>" +" <menuitem action='print'/>" +" </placeholder>" +" <placeholder name='custom-actions-2'>" +" <menuitem action='uri-copy'/>" +" <menuitem action='mailto-copy'/>" +" <menuitem action='image-copy'/>" +" </placeholder>" +" <placeholder name='custom-actions-3'/>" +" <separator/>" +" <menuitem action='select-all'/>" +" </popup>" +"</ui>"; + +/* Forward Declarations */ +static void e_web_view_gtkhtml_alert_sink_init (EAlertSinkInterface *interface); +static void e_web_view_gtkhtml_selectable_init (ESelectableInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EWebViewGtkHTML, + e_web_view_gtkhtml, + GTK_TYPE_HTML, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL) + G_IMPLEMENT_INTERFACE ( + E_TYPE_ALERT_SINK, + e_web_view_gtkhtml_alert_sink_init) + G_IMPLEMENT_INTERFACE ( + E_TYPE_SELECTABLE, + e_web_view_gtkhtml_selectable_init)) + +static EWebViewGtkHTMLRequest * +web_view_gtkhtml_request_new (EWebViewGtkHTML *web_view, + const gchar *uri, + GtkHTMLStream *stream) +{ + EWebViewGtkHTMLRequest *request; + GList *list; + + request = g_slice_new (EWebViewGtkHTMLRequest); + + /* Try to detect file paths posing as URIs. */ + if (*uri == '/') + request->file = g_file_new_for_path (uri); + else + request->file = g_file_new_for_uri (uri); + + request->web_view = g_object_ref (web_view); + request->cancellable = g_cancellable_new (); + request->input_stream = NULL; + request->output_stream = stream; + + list = request->web_view->priv->requests; + list = g_list_prepend (list, request); + request->web_view->priv->requests = list; + + return request; +} + +static void +web_view_gtkhtml_request_free (EWebViewGtkHTMLRequest *request) +{ + GList *list; + + list = request->web_view->priv->requests; + list = g_list_remove (list, request); + request->web_view->priv->requests = list; + + g_object_unref (request->file); + g_object_unref (request->web_view); + g_object_unref (request->cancellable); + + if (request->input_stream != NULL) + g_object_unref (request->input_stream); + + g_slice_free (EWebViewGtkHTMLRequest, request); +} + +static void +web_view_gtkhtml_request_cancel (EWebViewGtkHTMLRequest *request) +{ + g_cancellable_cancel (request->cancellable); +} + +static gboolean +web_view_gtkhtml_request_check_for_error (EWebViewGtkHTMLRequest *request, + GError *error) +{ + GtkHTML *html; + GtkHTMLStream *stream; + + if (error == NULL) + return FALSE; + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) { + /* use this error, but do not close the stream */ + g_error_free (error); + return TRUE; + } + + /* XXX Should we log errors that are not cancellations? */ + + html = GTK_HTML (request->web_view); + stream = request->output_stream; + + gtk_html_end (html, stream, GTK_HTML_STREAM_ERROR); + web_view_gtkhtml_request_free (request); + g_error_free (error); + + return TRUE; +} + +static void +web_view_gtkhtml_request_stream_read_cb (GInputStream *input_stream, + GAsyncResult *result, + EWebViewGtkHTMLRequest *request) +{ + gssize bytes_read; + GError *error = NULL; + + bytes_read = g_input_stream_read_finish (input_stream, result, &error); + + if (web_view_gtkhtml_request_check_for_error (request, error)) + return; + + if (bytes_read == 0) { + gtk_html_end ( + GTK_HTML (request->web_view), + request->output_stream, GTK_HTML_STREAM_OK); + web_view_gtkhtml_request_free (request); + return; + } + + gtk_html_write ( + GTK_HTML (request->web_view), + request->output_stream, request->buffer, bytes_read); + + g_input_stream_read_async ( + request->input_stream, request->buffer, + sizeof (request->buffer), G_PRIORITY_DEFAULT, + request->cancellable, (GAsyncReadyCallback) + web_view_gtkhtml_request_stream_read_cb, request); +} + +static void +web_view_gtkhtml_request_read_cb (GFile *file, + GAsyncResult *result, + EWebViewGtkHTMLRequest *request) +{ + GFileInputStream *input_stream; + GError *error = NULL; + + /* Input stream might be NULL, so don't use cast macro. */ + input_stream = g_file_read_finish (file, result, &error); + request->input_stream = (GInputStream *) input_stream; + + if (web_view_gtkhtml_request_check_for_error (request, error)) + return; + + g_input_stream_read_async ( + request->input_stream, request->buffer, + sizeof (request->buffer), G_PRIORITY_DEFAULT, + request->cancellable, (GAsyncReadyCallback) + web_view_gtkhtml_request_stream_read_cb, request); +} + +static void +action_copy_clipboard_cb (GtkAction *action, + EWebViewGtkHTML *web_view) +{ + e_web_view_gtkhtml_copy_clipboard (web_view); +} + +static void +action_http_open_cb (GtkAction *action, + EWebViewGtkHTML *web_view) +{ + const gchar *uri; + gpointer parent; + + parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + uri = e_web_view_gtkhtml_get_selected_uri (web_view); + g_return_if_fail (uri != NULL); + + e_show_uri (parent, uri); +} + +static void +action_mailto_copy_cb (GtkAction *action, + EWebViewGtkHTML *web_view) +{ + CamelURL *curl; + CamelInternetAddress *inet_addr; + GtkClipboard *clipboard; + const gchar *uri; + gchar *text; + + uri = e_web_view_gtkhtml_get_selected_uri (web_view); + g_return_if_fail (uri != NULL); + + /* This should work because we checked it in update_actions(). */ + curl = camel_url_new (uri, NULL); + g_return_if_fail (curl != NULL); + + inet_addr = camel_internet_address_new (); + camel_address_decode (CAMEL_ADDRESS (inet_addr), curl->path); + text = camel_address_format (CAMEL_ADDRESS (inet_addr)); + if (text == NULL || *text == '\0') + text = g_strdup (uri + strlen ("mailto:")); + + g_object_unref (inet_addr); + camel_url_free (curl); + + clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); + gtk_clipboard_set_text (clipboard, text, -1); + gtk_clipboard_store (clipboard); + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_text (clipboard, text, -1); + gtk_clipboard_store (clipboard); + + g_free (text); +} + +static void +action_select_all_cb (GtkAction *action, + EWebViewGtkHTML *web_view) +{ + e_web_view_gtkhtml_select_all (web_view); +} + +static void +action_send_message_cb (GtkAction *action, + EWebViewGtkHTML *web_view) +{ + const gchar *uri; + gpointer parent; + gboolean handled; + + parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + uri = e_web_view_gtkhtml_get_selected_uri (web_view); + g_return_if_fail (uri != NULL); + + handled = FALSE; + g_signal_emit (web_view, signals[PROCESS_MAILTO], 0, uri, &handled); + + if (!handled) + e_show_uri (parent, uri); +} + +static void +action_uri_copy_cb (GtkAction *action, + EWebViewGtkHTML *web_view) +{ + GtkClipboard *clipboard; + const gchar *uri; + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + uri = e_web_view_gtkhtml_get_selected_uri (web_view); + g_return_if_fail (uri != NULL); + + gtk_clipboard_set_text (clipboard, uri, -1); + gtk_clipboard_store (clipboard); +} + +static void +action_image_copy_cb (GtkAction *action, + EWebViewGtkHTML *web_view) +{ + GtkClipboard *clipboard; + GdkPixbufAnimation *animation; + GdkPixbuf *pixbuf; + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + animation = e_web_view_gtkhtml_get_cursor_image (web_view); + g_return_if_fail (animation != NULL); + + pixbuf = gdk_pixbuf_animation_get_static_image (animation); + if (!pixbuf) + return; + + gtk_clipboard_set_image (clipboard, pixbuf); + gtk_clipboard_store (clipboard); +} + +static GtkActionEntry uri_entries[] = { + + { "uri-copy", + GTK_STOCK_COPY, + N_("_Copy Link Location"), + NULL, + N_("Copy the link to the clipboard"), + G_CALLBACK (action_uri_copy_cb) } +}; + +static GtkActionEntry http_entries[] = { + + { "http-open", + "emblem-web", + N_("_Open Link in Browser"), + NULL, + N_("Open the link in a web browser"), + G_CALLBACK (action_http_open_cb) } +}; + +static GtkActionEntry mailto_entries[] = { + + { "mailto-copy", + GTK_STOCK_COPY, + N_("_Copy Email Address"), + NULL, + N_("Copy the email address to the clipboard"), + G_CALLBACK (action_mailto_copy_cb) }, + + { "send-message", + "mail-message-new", + N_("_Send New Message To..."), + NULL, + N_("Send a mail message to this address"), + G_CALLBACK (action_send_message_cb) } +}; + +static GtkActionEntry image_entries[] = { + + { "image-copy", + GTK_STOCK_COPY, + N_("_Copy Image"), + NULL, + N_("Copy the image to the clipboard"), + G_CALLBACK (action_image_copy_cb) } +}; + +static GtkActionEntry selection_entries[] = { + + { "copy-clipboard", + GTK_STOCK_COPY, + NULL, + NULL, + N_("Copy the selection"), + G_CALLBACK (action_copy_clipboard_cb) }, +}; + +static GtkActionEntry standard_entries[] = { + + { "select-all", + GTK_STOCK_SELECT_ALL, + NULL, + NULL, + N_("Select all text and images"), + G_CALLBACK (action_select_all_cb) } +}; + +static gboolean +web_view_gtkhtml_button_press_event_cb (EWebViewGtkHTML *web_view, + GdkEventButton *event, + GtkHTML *frame) +{ + gboolean event_handled = FALSE; + gchar *uri = NULL; + + if (event) { + GdkPixbufAnimation *anim; + + if (frame == NULL) + frame = GTK_HTML (web_view); + + anim = gtk_html_get_image_at (frame, event->x, event->y); + e_web_view_gtkhtml_set_cursor_image (web_view, anim); + if (anim != NULL) + g_object_unref (anim); + } + + if (event != NULL && event->button != 3) + return FALSE; + + /* Only extract a URI if no selection is active. Selected text + * implies the user is more likely to want to copy the selection + * to the clipboard than open a link within the selection. */ + if (!e_web_view_gtkhtml_is_selection_active (web_view)) + uri = e_web_view_gtkhtml_extract_uri (web_view, event, frame); + + if (uri != NULL && g_str_has_prefix (uri, "##")) { + g_free (uri); + return FALSE; + } + + g_signal_emit ( + web_view, signals[POPUP_EVENT], 0, + event, uri, &event_handled); + + g_free (uri); + + return event_handled; +} + +static void +web_view_gtkhtml_menu_item_select_cb (EWebViewGtkHTML *web_view, + GtkWidget *widget) +{ + GtkAction *action; + GtkActivatable *activatable; + const gchar *tooltip; + + activatable = GTK_ACTIVATABLE (widget); + action = gtk_activatable_get_related_action (activatable); + tooltip = gtk_action_get_tooltip (action); + + if (tooltip == NULL) + return; + + e_web_view_gtkhtml_status_message (web_view, tooltip); +} + +static void +web_view_gtkhtml_menu_item_deselect_cb (EWebViewGtkHTML *web_view) +{ + e_web_view_gtkhtml_status_message (web_view, NULL); +} + +static void +web_view_gtkhtml_connect_proxy_cb (EWebViewGtkHTML *web_view, + GtkAction *action, + GtkWidget *proxy) +{ + if (!GTK_IS_MENU_ITEM (proxy)) + return; + + g_signal_connect_swapped ( + proxy, "select", + G_CALLBACK (web_view_gtkhtml_menu_item_select_cb), web_view); + + g_signal_connect_swapped ( + proxy, "deselect", + G_CALLBACK (web_view_gtkhtml_menu_item_deselect_cb), web_view); +} + +static void +web_view_gtkhtml_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ANIMATE: + e_web_view_gtkhtml_set_animate ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_boolean (value)); + return; + + case PROP_CARET_MODE: + e_web_view_gtkhtml_set_caret_mode ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_boolean (value)); + return; + + case PROP_DISABLE_PRINTING: + e_web_view_gtkhtml_set_disable_printing ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_boolean (value)); + return; + + case PROP_DISABLE_SAVE_TO_DISK: + e_web_view_gtkhtml_set_disable_save_to_disk ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_boolean (value)); + return; + + case PROP_EDITABLE: + e_web_view_gtkhtml_set_editable ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_boolean (value)); + return; + + case PROP_INLINE_SPELLING: + e_web_view_gtkhtml_set_inline_spelling ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_boolean (value)); + return; + + case PROP_MAGIC_LINKS: + e_web_view_gtkhtml_set_magic_links ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_boolean (value)); + return; + + case PROP_MAGIC_SMILEYS: + e_web_view_gtkhtml_set_magic_smileys ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_boolean (value)); + return; + + case PROP_OPEN_PROXY: + e_web_view_gtkhtml_set_open_proxy ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_object (value)); + return; + + case PROP_PRINT_PROXY: + e_web_view_gtkhtml_set_print_proxy ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_object (value)); + return; + + case PROP_SAVE_AS_PROXY: + e_web_view_gtkhtml_set_save_as_proxy ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_object (value)); + return; + + case PROP_SELECTED_URI: + e_web_view_gtkhtml_set_selected_uri ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_string (value)); + return; + case PROP_CURSOR_IMAGE: + e_web_view_gtkhtml_set_cursor_image ( + E_WEB_VIEW_GTKHTML (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +web_view_gtkhtml_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ANIMATE: + g_value_set_boolean ( + value, e_web_view_gtkhtml_get_animate ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_CARET_MODE: + g_value_set_boolean ( + value, e_web_view_gtkhtml_get_caret_mode ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_COPY_TARGET_LIST: + g_value_set_boxed ( + value, e_web_view_gtkhtml_get_copy_target_list ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_DISABLE_PRINTING: + g_value_set_boolean ( + value, e_web_view_gtkhtml_get_disable_printing ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_DISABLE_SAVE_TO_DISK: + g_value_set_boolean ( + value, e_web_view_gtkhtml_get_disable_save_to_disk ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_EDITABLE: + g_value_set_boolean ( + value, e_web_view_gtkhtml_get_editable ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_INLINE_SPELLING: + g_value_set_boolean ( + value, e_web_view_gtkhtml_get_inline_spelling ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_MAGIC_LINKS: + g_value_set_boolean ( + value, e_web_view_gtkhtml_get_magic_links ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_MAGIC_SMILEYS: + g_value_set_boolean ( + value, e_web_view_gtkhtml_get_magic_smileys ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_OPEN_PROXY: + g_value_set_object ( + value, e_web_view_gtkhtml_get_open_proxy ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_PASTE_TARGET_LIST: + g_value_set_boxed ( + value, e_web_view_gtkhtml_get_paste_target_list ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_PRINT_PROXY: + g_value_set_object ( + value, e_web_view_gtkhtml_get_print_proxy ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_SAVE_AS_PROXY: + g_value_set_object ( + value, e_web_view_gtkhtml_get_save_as_proxy ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_SELECTED_URI: + g_value_set_string ( + value, e_web_view_gtkhtml_get_selected_uri ( + E_WEB_VIEW_GTKHTML (object))); + return; + + case PROP_CURSOR_IMAGE: + g_value_set_object ( + value, e_web_view_gtkhtml_get_cursor_image ( + E_WEB_VIEW_GTKHTML (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +web_view_gtkhtml_dispose (GObject *object) +{ + EWebViewGtkHTMLPrivate *priv; + + priv = E_WEB_VIEW_GTKHTML_GET_PRIVATE (object); + + if (priv->ui_manager != NULL) { + g_object_unref (priv->ui_manager); + priv->ui_manager = NULL; + } + + if (priv->open_proxy != NULL) { + g_object_unref (priv->open_proxy); + priv->open_proxy = NULL; + } + + if (priv->print_proxy != NULL) { + g_object_unref (priv->print_proxy); + priv->print_proxy = NULL; + } + + if (priv->save_as_proxy != NULL) { + g_object_unref (priv->save_as_proxy); + priv->save_as_proxy = NULL; + } + + if (priv->copy_target_list != NULL) { + gtk_target_list_unref (priv->copy_target_list); + priv->copy_target_list = NULL; + } + + if (priv->paste_target_list != NULL) { + gtk_target_list_unref (priv->paste_target_list); + priv->paste_target_list = NULL; + } + + if (priv->cursor_image != NULL) { + g_object_unref (priv->cursor_image); + priv->cursor_image = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_web_view_gtkhtml_parent_class)->dispose (object); +} + +static void +web_view_gtkhtml_finalize (GObject *object) +{ + EWebViewGtkHTMLPrivate *priv; + + priv = E_WEB_VIEW_GTKHTML_GET_PRIVATE (object); + + /* All URI requests should be complete or cancelled by now. */ + if (priv->requests != NULL) + g_warning ("Finalizing EWebViewGtkHTML with active URI requests"); + + g_free (priv->selected_uri); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_web_view_gtkhtml_parent_class)->finalize (object); +} + +static void +web_view_gtkhtml_constructed (GObject *object) +{ +#ifndef G_OS_WIN32 + GSettings *settings; + + settings = g_settings_new ("org.gnome.desktop.lockdown"); + + g_settings_bind ( + settings, "disable-printing", + object, "disable-printing", + G_SETTINGS_BIND_GET); + + g_settings_bind ( + settings, "disable-save-to-disk", + object, "disable-save-to-disk", + G_SETTINGS_BIND_GET); + + g_object_unref (settings); +#endif + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_web_view_gtkhtml_parent_class)->constructed (object); +} + +static gboolean +web_view_gtkhtml_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + GtkWidgetClass *widget_class; + EWebViewGtkHTML *web_view; + + web_view = E_WEB_VIEW_GTKHTML (widget); + + if (web_view_gtkhtml_button_press_event_cb (web_view, event, NULL)) + return TRUE; + + /* Chain up to parent's button_press_event() method. */ + widget_class = GTK_WIDGET_CLASS (e_web_view_gtkhtml_parent_class); + return widget_class->button_press_event (widget, event); +} + +static gboolean +web_view_gtkhtml_scroll_event (GtkWidget *widget, + GdkEventScroll *event) +{ + if (event->state & GDK_CONTROL_MASK) { + GdkScrollDirection direction = event->direction; + + #if GTK_CHECK_VERSION(3,3,18) + if (direction == GDK_SCROLL_SMOOTH) { + static gdouble total_delta_y = 0.0; + + total_delta_y += event->delta_y; + + if (total_delta_y >= 1.0) { + total_delta_y = 0.0; + direction = GDK_SCROLL_DOWN; + } else if (total_delta_y <= -1.0) { + total_delta_y = 0.0; + direction = GDK_SCROLL_UP; + } else { + return FALSE; + } + } + #endif + + switch (direction) { + case GDK_SCROLL_UP: + gtk_html_zoom_in (GTK_HTML (widget)); + return TRUE; + case GDK_SCROLL_DOWN: + gtk_html_zoom_out (GTK_HTML (widget)); + return TRUE; + default: + break; + } + } + + return FALSE; +} + +static void +web_view_gtkhtml_url_requested (GtkHTML *html, + const gchar *uri, + GtkHTMLStream *stream) +{ + EWebViewGtkHTMLRequest *request; + + request = web_view_gtkhtml_request_new (E_WEB_VIEW_GTKHTML (html), uri, stream); + + g_file_read_async ( + request->file, G_PRIORITY_DEFAULT, + request->cancellable, (GAsyncReadyCallback) + web_view_gtkhtml_request_read_cb, request); +} + +static void +web_view_gtkhtml_gtkhtml_link_clicked (GtkHTML *html, + const gchar *uri) +{ + EWebViewGtkHTMLClass *class; + EWebViewGtkHTML *web_view; + + web_view = E_WEB_VIEW_GTKHTML (html); + + class = E_WEB_VIEW_GTKHTML_GET_CLASS (web_view); + g_return_if_fail (class->link_clicked != NULL); + + class->link_clicked (web_view, uri); +} + +static void +web_view_gtkhtml_on_url (GtkHTML *html, + const gchar *uri) +{ + EWebViewGtkHTMLClass *class; + EWebViewGtkHTML *web_view; + + web_view = E_WEB_VIEW_GTKHTML (html); + + class = E_WEB_VIEW_GTKHTML_GET_CLASS (web_view); + g_return_if_fail (class->hovering_over_link != NULL); + + /* XXX WebKit would supply a title here. */ + class->hovering_over_link (web_view, NULL, uri); +} + +static void +web_view_gtkhtml_iframe_created (GtkHTML *html, + GtkHTML *iframe) +{ + g_signal_connect_swapped ( + iframe, "button-press-event", + G_CALLBACK (web_view_gtkhtml_button_press_event_cb), html); +} + +static gchar * +web_view_gtkhtml_extract_uri (EWebViewGtkHTML *web_view, + GdkEventButton *event, + GtkHTML *html) +{ + gchar *uri; + + if (event != NULL) + uri = gtk_html_get_url_at (html, event->x, event->y); + else + uri = gtk_html_get_cursor_url (html); + + return uri; +} + +static void +web_view_gtkhtml_hovering_over_link (EWebViewGtkHTML *web_view, + const gchar *title, + const gchar *uri) +{ + CamelInternetAddress *address; + CamelURL *curl; + const gchar *format = NULL; + gchar *message = NULL; + gchar *who; + + if (uri == NULL || *uri == '\0') + goto exit; + + if (g_str_has_prefix (uri, "mailto:")) + format = _("Click to mail %s"); + else if (g_str_has_prefix (uri, "callto:")) + format = _("Click to call %s"); + else if (g_str_has_prefix (uri, "h323:")) + format = _("Click to call %s"); + else if (g_str_has_prefix (uri, "sip:")) + format = _("Click to call %s"); + else if (g_str_has_prefix (uri, "##")) + message = g_strdup (_("Click to hide/unhide addresses")); + else + message = g_strdup_printf (_("Click to open %s"), uri); + + if (format == NULL) + goto exit; + + /* XXX Use something other than Camel here. Surely + * there's other APIs around that can do this. */ + curl = camel_url_new (uri, NULL); + address = camel_internet_address_new (); + camel_address_decode (CAMEL_ADDRESS (address), curl->path); + who = camel_address_format (CAMEL_ADDRESS (address)); + g_object_unref (address); + camel_url_free (curl); + + if (who == NULL) + who = g_strdup (strchr (uri, ':') + 1); + + message = g_strdup_printf (format, who); + + g_free (who); + +exit: + e_web_view_gtkhtml_status_message (web_view, message); + + g_free (message); +} + +static void +web_view_gtkhtml_link_clicked (EWebViewGtkHTML *web_view, + const gchar *uri) +{ + gpointer parent; + + parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + e_show_uri (parent, uri); +} + +static void +web_view_gtkhtml_load_string (EWebViewGtkHTML *web_view, + const gchar *string) +{ + if (string != NULL && *string != '\0') + gtk_html_load_from_string (GTK_HTML (web_view), string, -1); + else + e_web_view_gtkhtml_clear (web_view); +} + +static void +web_view_gtkhtml_copy_clipboard (EWebViewGtkHTML *web_view) +{ + gtk_html_command (GTK_HTML (web_view), "copy"); +} + +static void +web_view_gtkhtml_cut_clipboard (EWebViewGtkHTML *web_view) +{ + if (e_web_view_gtkhtml_get_editable (web_view)) + gtk_html_command (GTK_HTML (web_view), "cut"); +} + +static void +web_view_gtkhtml_paste_clipboard (EWebViewGtkHTML *web_view) +{ + if (e_web_view_gtkhtml_get_editable (web_view)) + gtk_html_command (GTK_HTML (web_view), "paste"); +} + +static gboolean +web_view_gtkhtml_popup_event (EWebViewGtkHTML *web_view, + GdkEventButton *event, + const gchar *uri) +{ + e_web_view_gtkhtml_set_selected_uri (web_view, uri); + e_web_view_gtkhtml_show_popup_menu (web_view, event, NULL, NULL); + + return TRUE; +} + +static void +web_view_gtkhtml_stop_loading (EWebViewGtkHTML *web_view) +{ + g_list_foreach ( + web_view->priv->requests, (GFunc) + web_view_gtkhtml_request_cancel, NULL); + + gtk_html_stop (GTK_HTML (web_view)); +} + +static void +web_view_gtkhtml_update_actions (EWebViewGtkHTML *web_view) +{ + GtkActionGroup *action_group; + gboolean have_selection; + gboolean scheme_is_http = FALSE; + gboolean scheme_is_mailto = FALSE; + gboolean uri_is_valid = FALSE; + gboolean has_cursor_image; + gboolean visible; + const gchar *group_name; + const gchar *uri; + + uri = e_web_view_gtkhtml_get_selected_uri (web_view); + have_selection = e_web_view_gtkhtml_is_selection_active (web_view); + has_cursor_image = e_web_view_gtkhtml_get_cursor_image (web_view) != NULL; + + /* Parse the URI early so we know if the actions will work. */ + if (uri != NULL) { + CamelURL *curl; + + curl = camel_url_new (uri, NULL); + uri_is_valid = (curl != NULL); + camel_url_free (curl); + + scheme_is_http = + (g_ascii_strncasecmp (uri, "http:", 5) == 0) || + (g_ascii_strncasecmp (uri, "https:", 6) == 0); + + scheme_is_mailto = + (g_ascii_strncasecmp (uri, "mailto:", 7) == 0); + } + + /* Allow copying the URI even if it's malformed. */ + group_name = "uri"; + visible = (uri != NULL) && !scheme_is_mailto; + action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "http"; + visible = uri_is_valid && scheme_is_http; + action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "mailto"; + visible = uri_is_valid && scheme_is_mailto; + action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "image"; + visible = has_cursor_image; + action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "selection"; + visible = have_selection; + action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "standard"; + visible = (uri == NULL); + action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "lockdown-printing"; + visible = (uri == NULL) && !web_view->priv->disable_printing; + action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "lockdown-save-to-disk"; + visible = (uri == NULL) && !web_view->priv->disable_save_to_disk; + action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); +} + +static void +web_view_gtkhtml_submit_alert (EAlertSink *alert_sink, + EAlert *alert) +{ + GtkIconInfo *icon_info; + EWebViewGtkHTML *web_view; + GtkWidget *dialog; + GString *buffer; + const gchar *icon_name = NULL; + const gchar *filename; + gpointer parent; + gchar *icon_uri; + gint size = 0; + GError *error = NULL; + + web_view = E_WEB_VIEW_GTKHTML (alert_sink); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + /* We use equivalent named icons instead of stock IDs, + * since it's easier to get the filename of the icon. */ + switch (e_alert_get_message_type (alert)) { + case GTK_MESSAGE_INFO: + icon_name = "dialog-information"; + break; + + case GTK_MESSAGE_WARNING: + icon_name = "dialog-warning"; + break; + + case GTK_MESSAGE_ERROR: + icon_name = "dialog-error"; + break; + + default: + dialog = e_alert_dialog_new (parent, alert); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + return; + } + + gtk_icon_size_lookup (GTK_ICON_SIZE_DIALOG, &size, NULL); + + icon_info = gtk_icon_theme_lookup_icon ( + gtk_icon_theme_get_default (), + icon_name, size, GTK_ICON_LOOKUP_NO_SVG); + g_return_if_fail (icon_info != NULL); + + filename = gtk_icon_info_get_filename (icon_info); + icon_uri = g_filename_to_uri (filename, NULL, &error); + + if (error != NULL) { + g_warning ("%s", error->message); + g_clear_error (&error); + } + + buffer = g_string_sized_new (512); + + g_string_append ( + buffer, + "<html>" + "<head>" + "<meta http-equiv=\"content-type\"" + " content=\"text/html; charset=utf-8\">" + "</head>" + "<body>"); + + g_string_append ( + buffer, + "<table bgcolor='#000000' width='100%'" + " cellpadding='1' cellspacing='0'>" + "<tr>" + "<td>" + "<table bgcolor='#dddddd' width='100%' cellpadding='6'>" + "<tr>"); + + g_string_append_printf ( + buffer, + "<tr>" + "<td valign='top'>" + "<img src='%s'/>" + "</td>" + "<td align='left' width='100%%'>" + "<h3>%s</h3>" + "%s" + "</td>" + "</tr>", + icon_uri, + e_alert_get_primary_text (alert), + e_alert_get_secondary_text (alert)); + + g_string_append ( + buffer, + "</table>" + "</td>" + "</tr>" + "</table>" + "</body>" + "</html>"); + + e_web_view_gtkhtml_load_string (web_view, buffer->str); + + g_string_free (buffer, TRUE); + + gtk_icon_info_free (icon_info); + g_free (icon_uri); +} + +static void +web_view_gtkhtml_selectable_update_actions (ESelectable *selectable, + EFocusTracker *focus_tracker, + GdkAtom *clipboard_targets, + gint n_clipboard_targets) +{ + EWebViewGtkHTML *web_view; + GtkAction *action; + /*GtkTargetList *target_list;*/ + gboolean can_paste = FALSE; + gboolean editable; + gboolean have_selection; + gboolean sensitive; + const gchar *tooltip; + /*gint ii;*/ + + web_view = E_WEB_VIEW_GTKHTML (selectable); + editable = e_web_view_gtkhtml_get_editable (web_view); + have_selection = e_web_view_gtkhtml_is_selection_active (web_view); + + /* XXX GtkHtml implements its own clipboard instead of using + * GDK_SELECTION_CLIPBOARD, so we don't get notifications + * when the clipboard contents change. The logic below + * is what we would do if GtkHtml worked properly. + * Instead, we need to keep the Paste action sensitive so + * its accelerator overrides GtkHtml's key binding. */ +#if 0 + target_list = e_selectable_get_paste_target_list (selectable); + for (ii = 0; ii < n_clipboard_targets && !can_paste; ii++) + can_paste = gtk_target_list_find ( + target_list, clipboard_targets[ii], NULL); +#endif + can_paste = TRUE; + + action = e_focus_tracker_get_cut_clipboard_action (focus_tracker); + sensitive = editable && have_selection; + tooltip = _("Cut the selection"); + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, tooltip); + + action = e_focus_tracker_get_copy_clipboard_action (focus_tracker); + sensitive = have_selection; + tooltip = _("Copy the selection"); + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, tooltip); + + action = e_focus_tracker_get_paste_clipboard_action (focus_tracker); + sensitive = editable && can_paste; + tooltip = _("Paste the clipboard"); + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, tooltip); + + action = e_focus_tracker_get_select_all_action (focus_tracker); + sensitive = TRUE; + tooltip = _("Select all text and images"); + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, tooltip); +} + +static void +web_view_gtkhtml_selectable_cut_clipboard (ESelectable *selectable) +{ + e_web_view_gtkhtml_cut_clipboard (E_WEB_VIEW_GTKHTML (selectable)); +} + +static void +web_view_gtkhtml_selectable_copy_clipboard (ESelectable *selectable) +{ + e_web_view_gtkhtml_copy_clipboard (E_WEB_VIEW_GTKHTML (selectable)); +} + +static void +web_view_gtkhtml_selectable_paste_clipboard (ESelectable *selectable) +{ + e_web_view_gtkhtml_paste_clipboard (E_WEB_VIEW_GTKHTML (selectable)); +} + +static void +web_view_gtkhtml_selectable_select_all (ESelectable *selectable) +{ + e_web_view_gtkhtml_select_all (E_WEB_VIEW_GTKHTML (selectable)); +} + +static void +e_web_view_gtkhtml_class_init (EWebViewGtkHTMLClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkHTMLClass *html_class; + + g_type_class_add_private (class, sizeof (EWebViewGtkHTMLPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = web_view_gtkhtml_set_property; + object_class->get_property = web_view_gtkhtml_get_property; + object_class->dispose = web_view_gtkhtml_dispose; + object_class->finalize = web_view_gtkhtml_finalize; + object_class->constructed = web_view_gtkhtml_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->button_press_event = web_view_gtkhtml_button_press_event; + widget_class->scroll_event = web_view_gtkhtml_scroll_event; + + html_class = GTK_HTML_CLASS (class); + html_class->url_requested = web_view_gtkhtml_url_requested; + html_class->link_clicked = web_view_gtkhtml_gtkhtml_link_clicked; + html_class->on_url = web_view_gtkhtml_on_url; + html_class->iframe_created = web_view_gtkhtml_iframe_created; + + class->extract_uri = web_view_gtkhtml_extract_uri; + class->hovering_over_link = web_view_gtkhtml_hovering_over_link; + class->link_clicked = web_view_gtkhtml_link_clicked; + class->load_string = web_view_gtkhtml_load_string; + class->copy_clipboard = web_view_gtkhtml_copy_clipboard; + class->cut_clipboard = web_view_gtkhtml_cut_clipboard; + class->paste_clipboard = web_view_gtkhtml_paste_clipboard; + class->popup_event = web_view_gtkhtml_popup_event; + class->stop_loading = web_view_gtkhtml_stop_loading; + class->update_actions = web_view_gtkhtml_update_actions; + + g_object_class_install_property ( + object_class, + PROP_ANIMATE, + g_param_spec_boolean ( + "animate", + "Animate Images", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_CARET_MODE, + g_param_spec_boolean ( + "caret-mode", + "Caret Mode", + NULL, + FALSE, + G_PARAM_READWRITE)); + + /* Inherited from ESelectableInterface */ + g_object_class_override_property ( + object_class, + PROP_COPY_TARGET_LIST, + "copy-target-list"); + + g_object_class_install_property ( + object_class, + PROP_DISABLE_PRINTING, + g_param_spec_boolean ( + "disable-printing", + "Disable Printing", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_DISABLE_SAVE_TO_DISK, + g_param_spec_boolean ( + "disable-save-to-disk", + "Disable Save-to-Disk", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_EDITABLE, + g_param_spec_boolean ( + "editable", + "Editable", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_INLINE_SPELLING, + g_param_spec_boolean ( + "inline-spelling", + "Inline Spelling", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MAGIC_LINKS, + g_param_spec_boolean ( + "magic-links", + "Magic Links", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MAGIC_SMILEYS, + g_param_spec_boolean ( + "magic-smileys", + "Magic Smileys", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_OPEN_PROXY, + g_param_spec_object ( + "open-proxy", + "Open Proxy", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + /* Inherited from ESelectableInterface */ + g_object_class_override_property ( + object_class, + PROP_PASTE_TARGET_LIST, + "paste-target-list"); + + g_object_class_install_property ( + object_class, + PROP_PRINT_PROXY, + g_param_spec_object ( + "print-proxy", + "Print Proxy", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SAVE_AS_PROXY, + g_param_spec_object ( + "save-as-proxy", + "Save As Proxy", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SELECTED_URI, + g_param_spec_string ( + "selected-uri", + "Selected URI", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_IMAGE, + g_param_spec_object ( + "cursor-image", + "Image animation at the mouse cursor", + NULL, + GDK_TYPE_PIXBUF_ANIMATION, + G_PARAM_READWRITE)); + + signals[COPY_CLIPBOARD] = g_signal_new ( + "copy-clipboard", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EWebViewGtkHTMLClass, copy_clipboard), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[CUT_CLIPBOARD] = g_signal_new ( + "cut-clipboard", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EWebViewGtkHTMLClass, cut_clipboard), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[PASTE_CLIPBOARD] = g_signal_new ( + "paste-clipboard", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EWebViewGtkHTMLClass, paste_clipboard), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[POPUP_EVENT] = g_signal_new ( + "popup-event", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewGtkHTMLClass, popup_event), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__BOXED_STRING, + G_TYPE_BOOLEAN, 2, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE, + G_TYPE_STRING); + + signals[STATUS_MESSAGE] = g_signal_new ( + "status-message", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewGtkHTMLClass, status_message), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + signals[STOP_LOADING] = g_signal_new ( + "stop-loading", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewGtkHTMLClass, stop_loading), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[UPDATE_ACTIONS] = g_signal_new ( + "update-actions", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewGtkHTMLClass, update_actions), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /* return TRUE when a signal handler processed the mailto URI */ + signals[PROCESS_MAILTO] = g_signal_new ( + "process-mailto", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewGtkHTMLClass, process_mailto), + NULL, NULL, + e_marshal_BOOLEAN__STRING, + G_TYPE_BOOLEAN, 1, G_TYPE_STRING); +} + +static void +e_web_view_gtkhtml_alert_sink_init (EAlertSinkInterface *interface) +{ + interface->submit_alert = web_view_gtkhtml_submit_alert; +} + +static void +e_web_view_gtkhtml_selectable_init (ESelectableInterface *interface) +{ + interface->update_actions = web_view_gtkhtml_selectable_update_actions; + interface->cut_clipboard = web_view_gtkhtml_selectable_cut_clipboard; + interface->copy_clipboard = web_view_gtkhtml_selectable_copy_clipboard; + interface->paste_clipboard = web_view_gtkhtml_selectable_paste_clipboard; + interface->select_all = web_view_gtkhtml_selectable_select_all; +} + +static void +e_web_view_gtkhtml_init (EWebViewGtkHTML *web_view) +{ + GtkUIManager *ui_manager; + GtkActionGroup *action_group; + GtkTargetList *target_list; + EPopupAction *popup_action; + const gchar *domain = GETTEXT_PACKAGE; + const gchar *id; + GError *error = NULL; + + web_view->priv = E_WEB_VIEW_GTKHTML_GET_PRIVATE (web_view); + + ui_manager = gtk_ui_manager_new (); + web_view->priv->ui_manager = ui_manager; + + g_signal_connect_swapped ( + ui_manager, "connect-proxy", + G_CALLBACK (web_view_gtkhtml_connect_proxy_cb), web_view); + + target_list = gtk_target_list_new (NULL, 0); + web_view->priv->copy_target_list = target_list; + + target_list = gtk_target_list_new (NULL, 0); + web_view->priv->paste_target_list = target_list; + + action_group = gtk_action_group_new ("uri"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, uri_entries, + G_N_ELEMENTS (uri_entries), web_view); + + action_group = gtk_action_group_new ("http"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, http_entries, + G_N_ELEMENTS (http_entries), web_view); + + action_group = gtk_action_group_new ("mailto"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, mailto_entries, + G_N_ELEMENTS (mailto_entries), web_view); + + action_group = gtk_action_group_new ("image"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, image_entries, + G_N_ELEMENTS (image_entries), web_view); + + action_group = gtk_action_group_new ("selection"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, selection_entries, + G_N_ELEMENTS (selection_entries), web_view); + + action_group = gtk_action_group_new ("standard"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, standard_entries, + G_N_ELEMENTS (standard_entries), web_view); + + popup_action = e_popup_action_new ("open"); + gtk_action_group_add_action (action_group, GTK_ACTION (popup_action)); + g_object_unref (popup_action); + + g_object_bind_property ( + web_view, "open-proxy", + popup_action, "related-action", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + /* Support lockdown. */ + + action_group = gtk_action_group_new ("lockdown-printing"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + popup_action = e_popup_action_new ("print"); + gtk_action_group_add_action (action_group, GTK_ACTION (popup_action)); + g_object_unref (popup_action); + + g_object_bind_property ( + web_view, "print-proxy", + popup_action, "related-action", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + action_group = gtk_action_group_new ("lockdown-save-to-disk"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + popup_action = e_popup_action_new ("save-as"); + gtk_action_group_add_action (action_group, GTK_ACTION (popup_action)); + g_object_unref (popup_action); + + g_object_bind_property ( + web_view, "save-as-proxy", + popup_action, "related-action", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + /* Because we are loading from a hard-coded string, there is + * no chance of I/O errors. Failure here implies a malformed + * UI definition. Full stop. */ + gtk_ui_manager_add_ui_from_string (ui_manager, ui, -1, &error); + if (error != NULL) + g_error ("%s", error->message); + + id = "org.gnome.evolution.webview"; + e_plugin_ui_register_manager (ui_manager, id, web_view); + e_plugin_ui_enable_manager (ui_manager, id); + + e_extensible_load_extensions (E_EXTENSIBLE (web_view)); +} + +GtkWidget * +e_web_view_gtkhtml_new (void) +{ + return g_object_new (E_TYPE_WEB_VIEW_GTKHTML, NULL); +} + +void +e_web_view_gtkhtml_clear (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_load_empty (GTK_HTML (web_view)); +} + +void +e_web_view_gtkhtml_load_string (EWebViewGtkHTML *web_view, + const gchar *string) +{ + EWebViewGtkHTMLClass *class; + + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + class = E_WEB_VIEW_GTKHTML_GET_CLASS (web_view); + g_return_if_fail (class->load_string != NULL); + + class->load_string (web_view, string); +} + +gboolean +e_web_view_gtkhtml_get_animate (EWebViewGtkHTML *web_view) +{ + /* XXX This is just here to maintain symmetry + * with e_web_view_set_animate(). */ + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return gtk_html_get_animate (GTK_HTML (web_view)); +} + +void +e_web_view_gtkhtml_set_animate (EWebViewGtkHTML *web_view, + gboolean animate) +{ + /* XXX GtkHTML does not utilize GObject properties as well + * as it could. This just wraps gtk_html_set_animate() + * so we can get a "notify::animate" signal. */ + + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_set_animate (GTK_HTML (web_view), animate); + + g_object_notify (G_OBJECT (web_view), "animate"); +} + +gboolean +e_web_view_gtkhtml_get_caret_mode (EWebViewGtkHTML *web_view) +{ + /* XXX This is just here to maintain symmetry + * with e_web_view_set_caret_mode(). */ + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return gtk_html_get_caret_mode (GTK_HTML (web_view)); +} + +void +e_web_view_gtkhtml_set_caret_mode (EWebViewGtkHTML *web_view, + gboolean caret_mode) +{ + /* XXX GtkHTML does not utilize GObject properties as well + * as it could. This just wraps gtk_html_set_caret_mode() + * so we can get a "notify::caret-mode" signal. */ + + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_set_caret_mode (GTK_HTML (web_view), caret_mode); + + g_object_notify (G_OBJECT (web_view), "caret-mode"); +} + +GtkTargetList * +e_web_view_gtkhtml_get_copy_target_list (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); + + return web_view->priv->copy_target_list; +} + +gboolean +e_web_view_gtkhtml_get_disable_printing (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return web_view->priv->disable_printing; +} + +void +e_web_view_gtkhtml_set_disable_printing (EWebViewGtkHTML *web_view, + gboolean disable_printing) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + web_view->priv->disable_printing = disable_printing; + + g_object_notify (G_OBJECT (web_view), "disable-printing"); +} + +gboolean +e_web_view_gtkhtml_get_disable_save_to_disk (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return web_view->priv->disable_save_to_disk; +} + +void +e_web_view_gtkhtml_set_disable_save_to_disk (EWebViewGtkHTML *web_view, + gboolean disable_save_to_disk) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + web_view->priv->disable_save_to_disk = disable_save_to_disk; + + g_object_notify (G_OBJECT (web_view), "disable-save-to-disk"); +} + +gboolean +e_web_view_gtkhtml_get_editable (EWebViewGtkHTML *web_view) +{ + /* XXX This is just here to maintain symmetry + * with e_web_view_set_editable(). */ + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return gtk_html_get_editable (GTK_HTML (web_view)); +} + +void +e_web_view_gtkhtml_set_editable (EWebViewGtkHTML *web_view, + gboolean editable) +{ + /* XXX GtkHTML does not utilize GObject properties as well + * as it could. This just wraps gtk_html_set_editable() + * so we can get a "notify::editable" signal. */ + + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_set_editable (GTK_HTML (web_view), editable); + + g_object_notify (G_OBJECT (web_view), "editable"); +} + +gboolean +e_web_view_gtkhtml_get_inline_spelling (EWebViewGtkHTML *web_view) +{ + /* XXX This is just here to maintain symmetry + * with e_web_view_set_inline_spelling(). */ + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return gtk_html_get_inline_spelling (GTK_HTML (web_view)); +} + +void +e_web_view_gtkhtml_set_inline_spelling (EWebViewGtkHTML *web_view, + gboolean inline_spelling) +{ + /* XXX GtkHTML does not utilize GObject properties as well + * as it could. This just wraps gtk_html_set_inline_spelling() + * so we get a "notify::inline-spelling" signal. */ + + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_set_inline_spelling (GTK_HTML (web_view), inline_spelling); + + g_object_notify (G_OBJECT (web_view), "inline-spelling"); +} + +gboolean +e_web_view_gtkhtml_get_magic_links (EWebViewGtkHTML *web_view) +{ + /* XXX This is just here to maintain symmetry + * with e_web_view_set_magic_links(). */ + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return gtk_html_get_magic_links (GTK_HTML (web_view)); +} + +void +e_web_view_gtkhtml_set_magic_links (EWebViewGtkHTML *web_view, + gboolean magic_links) +{ + /* XXX GtkHTML does not utilize GObject properties as well + * as it could. This just wraps gtk_html_set_magic_links() + * so we can get a "notify::magic-links" signal. */ + + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_set_magic_links (GTK_HTML (web_view), magic_links); + + g_object_notify (G_OBJECT (web_view), "magic-links"); +} + +gboolean +e_web_view_gtkhtml_get_magic_smileys (EWebViewGtkHTML *web_view) +{ + /* XXX This is just here to maintain symmetry + * with e_web_view_set_magic_smileys(). */ + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return gtk_html_get_magic_smileys (GTK_HTML (web_view)); +} + +void +e_web_view_gtkhtml_set_magic_smileys (EWebViewGtkHTML *web_view, + gboolean magic_smileys) +{ + /* XXX GtkHTML does not utilize GObject properties as well + * as it could. This just wraps gtk_html_set_magic_smileys() + * so we can get a "notify::magic-smileys" signal. */ + + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_set_magic_smileys (GTK_HTML (web_view), magic_smileys); + + g_object_notify (G_OBJECT (web_view), "magic-smileys"); +} + +const gchar * +e_web_view_gtkhtml_get_selected_uri (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); + + return web_view->priv->selected_uri; +} + +void +e_web_view_gtkhtml_set_selected_uri (EWebViewGtkHTML *web_view, + const gchar *selected_uri) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + g_free (web_view->priv->selected_uri); + web_view->priv->selected_uri = g_strdup (selected_uri); + + g_object_notify (G_OBJECT (web_view), "selected-uri"); +} + +GdkPixbufAnimation * +e_web_view_gtkhtml_get_cursor_image (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); + + return web_view->priv->cursor_image; +} + +void +e_web_view_gtkhtml_set_cursor_image (EWebViewGtkHTML *web_view, + GdkPixbufAnimation *image) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + if (image != NULL) + g_object_ref (image); + + if (web_view->priv->cursor_image != NULL) + g_object_unref (web_view->priv->cursor_image); + + web_view->priv->cursor_image = image; + + g_object_notify (G_OBJECT (web_view), "cursor-image"); +} + +GtkAction * +e_web_view_gtkhtml_get_open_proxy (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return web_view->priv->open_proxy; +} + +void +e_web_view_gtkhtml_set_open_proxy (EWebViewGtkHTML *web_view, + GtkAction *open_proxy) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + if (open_proxy != NULL) { + g_return_if_fail (GTK_IS_ACTION (open_proxy)); + g_object_ref (open_proxy); + } + + if (web_view->priv->open_proxy != NULL) + g_object_unref (web_view->priv->open_proxy); + + web_view->priv->open_proxy = open_proxy; + + g_object_notify (G_OBJECT (web_view), "open-proxy"); +} + +GtkTargetList * +e_web_view_gtkhtml_get_paste_target_list (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); + + return web_view->priv->paste_target_list; +} + +GtkAction * +e_web_view_gtkhtml_get_print_proxy (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return web_view->priv->print_proxy; +} + +void +e_web_view_gtkhtml_set_print_proxy (EWebViewGtkHTML *web_view, + GtkAction *print_proxy) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + if (print_proxy != NULL) { + g_return_if_fail (GTK_IS_ACTION (print_proxy)); + g_object_ref (print_proxy); + } + + if (web_view->priv->print_proxy != NULL) + g_object_unref (web_view->priv->print_proxy); + + web_view->priv->print_proxy = print_proxy; + + g_object_notify (G_OBJECT (web_view), "print-proxy"); +} + +GtkAction * +e_web_view_gtkhtml_get_save_as_proxy (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return web_view->priv->save_as_proxy; +} + +void +e_web_view_gtkhtml_set_save_as_proxy (EWebViewGtkHTML *web_view, + GtkAction *save_as_proxy) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + if (save_as_proxy != NULL) { + g_return_if_fail (GTK_IS_ACTION (save_as_proxy)); + g_object_ref (save_as_proxy); + } + + if (web_view->priv->save_as_proxy != NULL) + g_object_unref (web_view->priv->save_as_proxy); + + web_view->priv->save_as_proxy = save_as_proxy; + + g_object_notify (G_OBJECT (web_view), "save-as-proxy"); +} + +GtkAction * +e_web_view_gtkhtml_get_action (EWebViewGtkHTML *web_view, + const gchar *action_name) +{ + GtkUIManager *ui_manager; + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); + g_return_val_if_fail (action_name != NULL, NULL); + + ui_manager = e_web_view_gtkhtml_get_ui_manager (web_view); + + return e_lookup_action (ui_manager, action_name); +} + +GtkActionGroup * +e_web_view_gtkhtml_get_action_group (EWebViewGtkHTML *web_view, + const gchar *group_name) +{ + GtkUIManager *ui_manager; + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); + g_return_val_if_fail (group_name != NULL, NULL); + + ui_manager = e_web_view_gtkhtml_get_ui_manager (web_view); + + return e_lookup_action_group (ui_manager, group_name); +} + +gchar * +e_web_view_gtkhtml_extract_uri (EWebViewGtkHTML *web_view, + GdkEventButton *event, + GtkHTML *frame) +{ + EWebViewGtkHTMLClass *class; + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); + + if (frame == NULL) + frame = GTK_HTML (web_view); + + class = E_WEB_VIEW_GTKHTML_GET_CLASS (web_view); + g_return_val_if_fail (class->extract_uri != NULL, NULL); + + return class->extract_uri (web_view, event, frame); +} + +void +e_web_view_gtkhtml_copy_clipboard (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + g_signal_emit (web_view, signals[COPY_CLIPBOARD], 0); +} + +void +e_web_view_gtkhtml_cut_clipboard (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + g_signal_emit (web_view, signals[CUT_CLIPBOARD], 0); +} + +gboolean +e_web_view_gtkhtml_is_selection_active (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return gtk_html_command (GTK_HTML (web_view), "is-selection-active"); +} + +void +e_web_view_gtkhtml_paste_clipboard (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + g_signal_emit (web_view, signals[PASTE_CLIPBOARD], 0); +} + +gboolean +e_web_view_gtkhtml_scroll_forward (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return gtk_html_command (GTK_HTML (web_view), "scroll-forward"); +} + +gboolean +e_web_view_gtkhtml_scroll_backward (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); + + return gtk_html_command (GTK_HTML (web_view), "scroll-backward"); +} + +void +e_web_view_gtkhtml_select_all (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_command (GTK_HTML (web_view), "select-all"); +} + +void +e_web_view_gtkhtml_unselect_all (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_command (GTK_HTML (web_view), "unselect-all"); +} + +void +e_web_view_gtkhtml_zoom_100 (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_command (GTK_HTML (web_view), "zoom-reset"); +} + +void +e_web_view_gtkhtml_zoom_in (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_command (GTK_HTML (web_view), "zoom-in"); +} + +void +e_web_view_gtkhtml_zoom_out (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + gtk_html_command (GTK_HTML (web_view), "zoom-out"); +} + +GtkUIManager * +e_web_view_gtkhtml_get_ui_manager (EWebViewGtkHTML *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); + + return web_view->priv->ui_manager; +} + +GtkWidget * +e_web_view_gtkhtml_get_popup_menu (EWebViewGtkHTML *web_view) +{ + GtkUIManager *ui_manager; + GtkWidget *menu; + + g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); + + ui_manager = e_web_view_gtkhtml_get_ui_manager (web_view); + menu = gtk_ui_manager_get_widget (ui_manager, "/context"); + g_return_val_if_fail (GTK_IS_MENU (menu), NULL); + + return menu; +} + +void +e_web_view_gtkhtml_show_popup_menu (EWebViewGtkHTML *web_view, + GdkEventButton *event, + GtkMenuPositionFunc func, + gpointer user_data) +{ + GtkWidget *menu; + + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + e_web_view_gtkhtml_update_actions (web_view); + + menu = e_web_view_gtkhtml_get_popup_menu (web_view); + + if (event != NULL) + gtk_menu_popup ( + GTK_MENU (menu), NULL, NULL, func, + user_data, event->button, event->time); + else + gtk_menu_popup ( + GTK_MENU (menu), NULL, NULL, func, + user_data, 0, gtk_get_current_event_time ()); +} + +void +e_web_view_gtkhtml_status_message (EWebViewGtkHTML *web_view, + const gchar *status_message) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + g_signal_emit (web_view, signals[STATUS_MESSAGE], 0, status_message); +} + +void +e_web_view_gtkhtml_stop_loading (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + g_signal_emit (web_view, signals[STOP_LOADING], 0); +} + +void +e_web_view_gtkhtml_update_actions (EWebViewGtkHTML *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); + + g_signal_emit (web_view, signals[UPDATE_ACTIONS], 0); +} diff --git a/e-util/e-web-view-gtkhtml.h b/e-util/e-web-view-gtkhtml.h new file mode 100644 index 0000000000..ebe965c61b --- /dev/null +++ b/e-util/e-web-view-gtkhtml.h @@ -0,0 +1,177 @@ +/* + * e-web-view-gtkhtml.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +/* This is intended to serve as a common base class for all HTML viewing + * needs in Evolution. Currently based on GtkHTML, the idea is to wrap + * the GtkHTML API enough that we no longer have to make direct calls to + * it. This should help smooth the transition to WebKit/GTK+. + * + * This class handles basic tasks like mouse hovers over links, clicked + * links, and servicing URI requests asynchronously via GIO. */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_WEB_VIEW_GTKHTML_H +#define E_WEB_VIEW_GTKHTML_H + +#include <gtkhtml/gtkhtml.h> + +/* Standard GObject macros */ +#define E_TYPE_WEB_VIEW_GTKHTML \ + (e_web_view_gtkhtml_get_type ()) +#define E_WEB_VIEW_GTKHTML(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_WEB_VIEW_GTKHTML, EWebViewGtkHTML)) +#define E_WEB_VIEW_GTKHTML_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_WEB_VIEW_GTKHTML, EWebViewGtkHTMLClass)) +#define E_IS_WEB_VIEW_GTKHTML(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_WEB_VIEW_GTKHTML)) +#define E_IS_WEB_VIEW_GTKHTML_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_WEB_VIEW_GTKHTML)) +#define E_WEB_VIEW_GTKHTML_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_WEB_VIEW_GTKHTML, EWebViewGtkHTMLClass)) + +G_BEGIN_DECLS + +typedef struct _EWebViewGtkHTML EWebViewGtkHTML; +typedef struct _EWebViewGtkHTMLClass EWebViewGtkHTMLClass; +typedef struct _EWebViewGtkHTMLPrivate EWebViewGtkHTMLPrivate; + +struct _EWebViewGtkHTML { + GtkHTML parent; + EWebViewGtkHTMLPrivate *priv; +}; + +struct _EWebViewGtkHTMLClass { + GtkHTMLClass parent_class; + + /* Methods */ + gchar * (*extract_uri) (EWebViewGtkHTML *web_view, + GdkEventButton *event, + GtkHTML *frame); + void (*hovering_over_link) (EWebViewGtkHTML *web_view, + const gchar *title, + const gchar *uri); + void (*link_clicked) (EWebViewGtkHTML *web_view, + const gchar *uri); + void (*load_string) (EWebViewGtkHTML *web_view, + const gchar *load_string); + + /* Signals */ + void (*copy_clipboard) (EWebViewGtkHTML *web_view); + void (*cut_clipboard) (EWebViewGtkHTML *web_view); + void (*paste_clipboard) (EWebViewGtkHTML *web_view); + gboolean (*popup_event) (EWebViewGtkHTML *web_view, + GdkEventButton *event, + const gchar *uri); + void (*status_message) (EWebViewGtkHTML *web_view, + const gchar *status_message); + void (*stop_loading) (EWebViewGtkHTML *web_view); + void (*update_actions) (EWebViewGtkHTML *web_view); + gboolean (*process_mailto) (EWebViewGtkHTML *web_view, + const gchar *mailto_uri); +}; + +GType e_web_view_gtkhtml_get_type (void); +GtkWidget * e_web_view_gtkhtml_new (void); +void e_web_view_gtkhtml_clear (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_load_string (EWebViewGtkHTML *web_view, + const gchar *string); +gboolean e_web_view_gtkhtml_get_animate (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_animate (EWebViewGtkHTML *web_view, + gboolean animate); +gboolean e_web_view_gtkhtml_get_caret_mode (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_caret_mode (EWebViewGtkHTML *web_view, + gboolean caret_mode); +GtkTargetList * e_web_view_gtkhtml_get_copy_target_list (EWebViewGtkHTML *web_view); +gboolean e_web_view_gtkhtml_get_disable_printing (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_disable_printing (EWebViewGtkHTML *web_view, + gboolean disable_printing); +gboolean e_web_view_gtkhtml_get_disable_save_to_disk + (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_disable_save_to_disk + (EWebViewGtkHTML *web_view, + gboolean disable_save_to_disk); +gboolean e_web_view_gtkhtml_get_editable (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_editable (EWebViewGtkHTML *web_view, + gboolean editable); +gboolean e_web_view_gtkhtml_get_inline_spelling (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_inline_spelling (EWebViewGtkHTML *web_view, + gboolean inline_spelling); +gboolean e_web_view_gtkhtml_get_magic_links (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_magic_links (EWebViewGtkHTML *web_view, + gboolean magic_links); +gboolean e_web_view_gtkhtml_get_magic_smileys (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_magic_smileys (EWebViewGtkHTML *web_view, + gboolean magic_smileys); +const gchar * e_web_view_gtkhtml_get_selected_uri (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_selected_uri (EWebViewGtkHTML *web_view, + const gchar *selected_uri); +GdkPixbufAnimation * + e_web_view_gtkhtml_get_cursor_image (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_cursor_image (EWebViewGtkHTML *web_view, + GdkPixbufAnimation *animation); +GtkAction * e_web_view_gtkhtml_get_open_proxy (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_open_proxy (EWebViewGtkHTML *web_view, + GtkAction *open_proxy); +GtkTargetList * e_web_view_gtkhtml_get_paste_target_list + (EWebViewGtkHTML *web_view); +GtkAction * e_web_view_gtkhtml_get_print_proxy (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_print_proxy (EWebViewGtkHTML *web_view, + GtkAction *print_proxy); +GtkAction * e_web_view_gtkhtml_get_save_as_proxy (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_set_save_as_proxy (EWebViewGtkHTML *web_view, + GtkAction *save_as_proxy); +GtkAction * e_web_view_gtkhtml_get_action (EWebViewGtkHTML *web_view, + const gchar *action_name); +GtkActionGroup *e_web_view_gtkhtml_get_action_group (EWebViewGtkHTML *web_view, + const gchar *group_name); +gchar * e_web_view_gtkhtml_extract_uri (EWebViewGtkHTML *web_view, + GdkEventButton *event, + GtkHTML *frame); +void e_web_view_gtkhtml_copy_clipboard (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_cut_clipboard (EWebViewGtkHTML *web_view); +gboolean e_web_view_gtkhtml_is_selection_active (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_paste_clipboard (EWebViewGtkHTML *web_view); +gboolean e_web_view_gtkhtml_scroll_forward (EWebViewGtkHTML *web_view); +gboolean e_web_view_gtkhtml_scroll_backward (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_select_all (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_unselect_all (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_zoom_100 (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_zoom_in (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_zoom_out (EWebViewGtkHTML *web_view); +GtkUIManager * e_web_view_gtkhtml_get_ui_manager (EWebViewGtkHTML *web_view); +GtkWidget * e_web_view_gtkhtml_get_popup_menu (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_show_popup_menu (EWebViewGtkHTML *web_view, + GdkEventButton *event, + GtkMenuPositionFunc func, + gpointer user_data); +void e_web_view_gtkhtml_status_message (EWebViewGtkHTML *web_view, + const gchar *status_message); +void e_web_view_gtkhtml_stop_loading (EWebViewGtkHTML *web_view); +void e_web_view_gtkhtml_update_actions (EWebViewGtkHTML *web_view); + +G_END_DECLS + +#endif /* E_WEB_VIEW_GTKHTML_H */ diff --git a/e-util/e-web-view-preview.c b/e-util/e-web-view-preview.c new file mode 100644 index 0000000000..b75814fa83 --- /dev/null +++ b/e-util/e-web-view-preview.c @@ -0,0 +1,474 @@ +/* + * e-web-view-preview.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-web-view-preview.h" + +#include <string.h> +#include <glib/gi18n-lib.h> + +#define E_WEB_VIEW_PREVIEW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_WEB_VIEW_PREVIEW, EWebViewPreviewPrivate)) + +struct _EWebViewPreviewPrivate { + gboolean escape_values; + GString *updating_content; /* is NULL when not between begin_update/end_update */ +}; + +enum { + PROP_0, + PROP_TREE_VIEW, + PROP_PREVIEW_WIDGET, + PROP_ESCAPE_VALUES +}; + +G_DEFINE_TYPE ( + EWebViewPreview, + e_web_view_preview, + GTK_TYPE_VPANED); + +static void +web_view_preview_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ESCAPE_VALUES: + e_web_view_preview_set_escape_values ( + E_WEB_VIEW_PREVIEW (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +web_view_preview_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_TREE_VIEW: + g_value_set_object ( + value, e_web_view_preview_get_tree_view ( + E_WEB_VIEW_PREVIEW (object))); + return; + + case PROP_PREVIEW_WIDGET: + g_value_set_object ( + value, e_web_view_preview_get_preview ( + E_WEB_VIEW_PREVIEW (object))); + return; + + case PROP_ESCAPE_VALUES: + g_value_set_boolean ( + value, e_web_view_preview_get_escape_values ( + E_WEB_VIEW_PREVIEW (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +web_view_preview_dispose (GObject *object) +{ + EWebViewPreviewPrivate *priv; + + priv = E_WEB_VIEW_PREVIEW_GET_PRIVATE (object); + + if (priv->updating_content != NULL) { + g_string_free (priv->updating_content, TRUE); + priv->updating_content = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_web_view_preview_parent_class)->dispose (object); +} + +static void +e_web_view_preview_class_init (EWebViewPreviewClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EWebViewPreviewPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = web_view_preview_set_property; + object_class->get_property = web_view_preview_get_property; + object_class->dispose = web_view_preview_dispose; + + g_object_class_install_property ( + object_class, + PROP_TREE_VIEW, + g_param_spec_object ( + "tree-view", + "Tree View", + NULL, + GTK_TYPE_TREE_VIEW, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_PREVIEW_WIDGET, + g_param_spec_object ( + "preview-widget", + "Preview Widget", + NULL, + GTK_TYPE_WIDGET, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_ESCAPE_VALUES, + g_param_spec_boolean ( + "escape-values", + "Whether escaping values automatically, when inserting", + NULL, + TRUE, + G_PARAM_READWRITE)); +} + +static GtkWidget * +in_scrolled_window (GtkWidget *widget) +{ + GtkWidget *sw; + + g_return_val_if_fail (widget != NULL, NULL); + + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_container_add (GTK_CONTAINER (sw), widget); + + gtk_widget_show (widget); + gtk_widget_show (sw); + + return sw; +} + +static void +e_web_view_preview_init (EWebViewPreview *preview) +{ + GtkWidget *tree_view_sw, *web_view_sw; + + preview->priv = E_WEB_VIEW_PREVIEW_GET_PRIVATE (preview); + preview->priv->escape_values = TRUE; + + tree_view_sw = in_scrolled_window (gtk_tree_view_new ()); + web_view_sw = in_scrolled_window (e_web_view_new ()); + + gtk_widget_hide (tree_view_sw); + gtk_widget_show (web_view_sw); + + gtk_paned_pack1 (GTK_PANED (preview), tree_view_sw, FALSE, TRUE); + gtk_paned_pack2 (GTK_PANED (preview), web_view_sw, TRUE, TRUE); + + /* rawly 3 lines of a text plus a little bit more */ + if (gtk_paned_get_position (GTK_PANED (preview)) < 85) + gtk_paned_set_position (GTK_PANED (preview), 85); +} + +GtkWidget * +e_web_view_preview_new (void) +{ + return g_object_new (E_TYPE_WEB_VIEW_PREVIEW, NULL); +} + +GtkTreeView * +e_web_view_preview_get_tree_view (EWebViewPreview *preview) +{ + g_return_val_if_fail (preview != NULL, NULL); + g_return_val_if_fail (E_IS_WEB_VIEW_PREVIEW (preview), NULL); + + return GTK_TREE_VIEW (gtk_bin_get_child (GTK_BIN (gtk_paned_get_child1 (GTK_PANED (preview))))); +} + +GtkWidget * +e_web_view_preview_get_preview (EWebViewPreview *preview) +{ + g_return_val_if_fail (preview != NULL, NULL); + g_return_val_if_fail (E_IS_WEB_VIEW_PREVIEW (preview), NULL); + + return gtk_bin_get_child (GTK_BIN (gtk_paned_get_child2 (GTK_PANED (preview)))); +} + +void +e_web_view_preview_set_preview (EWebViewPreview *preview, + GtkWidget *preview_widget) +{ + GtkWidget *old_child; + + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + g_return_if_fail (GTK_IS_WIDGET (preview_widget)); + + old_child = gtk_bin_get_child (GTK_BIN (gtk_paned_get_child2 (GTK_PANED (preview)))); + if (old_child) { + g_return_if_fail (old_child != preview_widget); + gtk_widget_destroy (old_child); + } + + gtk_container_add (GTK_CONTAINER (gtk_paned_get_child2 (GTK_PANED (preview))), preview_widget); +} + +void +e_web_view_preview_show_tree_view (EWebViewPreview *preview) +{ + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + + gtk_widget_show (gtk_paned_get_child1 (GTK_PANED (preview))); +} + +void +e_web_view_preview_hide_tree_view (EWebViewPreview *preview) +{ + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + + gtk_widget_hide (gtk_paned_get_child1 (GTK_PANED (preview))); +} + +void +e_web_view_preview_set_escape_values (EWebViewPreview *preview, + gboolean escape) +{ + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + + preview->priv->escape_values = escape; +} + +gboolean +e_web_view_preview_get_escape_values (EWebViewPreview *preview) +{ + g_return_val_if_fail (preview != NULL, FALSE); + g_return_val_if_fail (E_IS_WEB_VIEW_PREVIEW (preview), FALSE); + g_return_val_if_fail (preview->priv != NULL, FALSE); + + return preview->priv->escape_values; +} + +void +e_web_view_preview_begin_update (EWebViewPreview *preview) +{ + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + + if (preview->priv->updating_content) { + g_warning ("%s: Previous content update isn't finished with e_web_view_preview_end_update()", G_STRFUNC); + g_string_free (preview->priv->updating_content, TRUE); + } + + preview->priv->updating_content = g_string_new ("<TABLE width=\"100%\" border=\"0\" cols=\"2\">"); +} + +void +e_web_view_preview_end_update (EWebViewPreview *preview) +{ + GtkWidget *web_view; + + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + g_return_if_fail (preview->priv->updating_content != NULL); + + g_string_append (preview->priv->updating_content, "</TABLE>"); + + web_view = e_web_view_preview_get_preview (preview); + if (E_IS_WEB_VIEW (web_view)) + e_web_view_load_string (E_WEB_VIEW (web_view), preview->priv->updating_content->str); + + g_string_free (preview->priv->updating_content, TRUE); + preview->priv->updating_content = NULL; +} + +static gchar * +replace_string (const gchar *text, + const gchar *find, + const gchar *replace) +{ + const gchar *p, *next; + GString *str; + gint find_len; + + g_return_val_if_fail (text != NULL, NULL); + g_return_val_if_fail (find != NULL, NULL); + g_return_val_if_fail (*find, NULL); + + find_len = strlen (find); + str = g_string_new (""); + + p = text; + while (next = strstr (p, find), next) { + if (p + 1 < next) + g_string_append_len (str, p, next - p); + + if (replace && *replace) + g_string_append (str, replace); + + p = next + find_len; + } + + g_string_append (str, p); + + return g_string_free (str, FALSE); +} + +static gchar * +web_view_preview_escape_text (EWebViewPreview *preview, + const gchar *text) +{ + gchar *utf8_valid, *res, *end; + + if (!e_web_view_preview_get_escape_values (preview)) + return NULL; + + g_return_val_if_fail (text != NULL, NULL); + + if (g_utf8_validate (text, -1, NULL)) { + res = g_markup_escape_text (text, -1); + } else { + utf8_valid = g_strdup (text); + while (end = NULL, !g_utf8_validate (utf8_valid, -1, (const gchar **) &end) && end && *end) + *end = '?'; + + res = g_markup_escape_text (utf8_valid, -1); + + g_free (utf8_valid); + } + + if (res && strchr (res, '\n')) { + /* replace line breaks with <BR> */ + if (strchr (res, '\r')) { + end = replace_string (res, "\r", ""); + g_free (res); + res = end; + } + + end = replace_string (res, "\n", "<BR>"); + g_free (res); + res = end; + } + + return res; +} + +void +e_web_view_preview_add_header (EWebViewPreview *preview, + gint index, + const gchar *header) +{ + gchar *escaped; + + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + g_return_if_fail (preview->priv->updating_content != NULL); + g_return_if_fail (header != NULL); + + if (index < 1) + index = 1; + else if (index > 6) + index = 6; + + escaped = web_view_preview_escape_text (preview, header); + if (escaped) + header = escaped; + + g_string_append_printf (preview->priv->updating_content, "<TR><TD colspan=2><H%d>%s</H%d></TD></TR>", index, header, index); + + g_free (escaped); +} + +void +e_web_view_preview_add_text (EWebViewPreview *preview, + const gchar *text) +{ + gchar *escaped; + + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + g_return_if_fail (preview->priv->updating_content != NULL); + g_return_if_fail (text != NULL); + + escaped = web_view_preview_escape_text (preview, text); + if (escaped) + text = escaped; + + g_string_append_printf (preview->priv->updating_content, "<TR><TD colspan=2><FONT size=\"3\">%s</FONT></TD></TR>", text); + + g_free (escaped); +} + +void +e_web_view_preview_add_raw_html (EWebViewPreview *preview, + const gchar *raw_html) +{ + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + g_return_if_fail (preview->priv->updating_content != NULL); + g_return_if_fail (raw_html != NULL); + + g_string_append_printf (preview->priv->updating_content, "<TR><TD colspan=2>%s</TD></TR>", raw_html); +} + +void +e_web_view_preview_add_separator (EWebViewPreview *preview) +{ + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + g_return_if_fail (preview->priv->updating_content != NULL); + + g_string_append (preview->priv->updating_content, "<TR><TD colspan=2><HR></TD></TR>"); +} + +void +e_web_view_preview_add_empty_line (EWebViewPreview *preview) +{ + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + g_return_if_fail (preview->priv->updating_content != NULL); + + g_string_append (preview->priv->updating_content, "<TR><TD colspan=2> </TD></TR>"); +} + +/* section can be NULL, but value cannot */ +void +e_web_view_preview_add_section (EWebViewPreview *preview, + const gchar *section, + const gchar *value) +{ + gchar *escaped_section = NULL, *escaped_value; + + g_return_if_fail (E_IS_WEB_VIEW_PREVIEW (preview)); + g_return_if_fail (preview->priv->updating_content != NULL); + g_return_if_fail (value != NULL); + + if (section) { + escaped_section = web_view_preview_escape_text (preview, section); + if (escaped_section) + section = escaped_section; + } + + escaped_value = web_view_preview_escape_text (preview, value); + if (escaped_value) + value = escaped_value; + + g_string_append_printf (preview->priv->updating_content, "<TR><TD width=\"10%%\" valign=\"top\" nowrap><FONT size=\"3\"><B>%s</B></FONT></TD><TD width=\"90%%\"><FONT size=\"3\">%s</FONT></TD></TR>", section ? section : "", value); + + g_free (escaped_section); + g_free (escaped_value); +} diff --git a/e-util/e-web-view-preview.h b/e-util/e-web-view-preview.h new file mode 100644 index 0000000000..54963b6fe6 --- /dev/null +++ b/e-util/e-web-view-preview.h @@ -0,0 +1,115 @@ +/* + * e-web-view-preview.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* This is intended to serve as a common widget for previews before import. + * It contains a GtkTreeView at the top and an EWebView at the bottom. + * The tree view is not shown initially, it should be forced with + * e_web_view_preview_show_tree_view(). + * + * The internal default EWebView can be accessed by e_web_view_preview_get_preview() + * and it should be updated for each change of the selected item in the tree + * view, when it's shown. + * + * Updating an EWebView content through helper functions of an EWebViewPreview + * begins with call of e_web_view_preview_begin_update(), which starts an empty + * page construction, which is finished by e_web_view_preview_end_update(), + * and the content of the EWebView is updated. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_WEB_VIEW_PREVIEW_H +#define E_WEB_VIEW_PREVIEW_H + +#include <e-util/e-web-view.h> + +/* Standard GObject macros */ +#define E_TYPE_WEB_VIEW_PREVIEW \ + (e_web_view_preview_get_type ()) +#define E_WEB_VIEW_PREVIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_WEB_VIEW_PREVIEW, EWebViewPreview)) +#define E_WEB_VIEW_PREVIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_WEB_VIEW_PREVIEW, EWebViewPreviewClass)) +#define E_IS_WEB_VIEW_PREVIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_WEB_VIEW_PREVIEW)) +#define E_IS_WEB_VIEW_PREVIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_WEB_VIEW_PREVIEW)) +#define E_WEB_VIEW_PREVIEW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_WEB_VIEW_PREVIEW, EWebViewPreviewClass)) + +G_BEGIN_DECLS + +typedef struct _EWebViewPreview EWebViewPreview; +typedef struct _EWebViewPreviewClass EWebViewPreviewClass; +typedef struct _EWebViewPreviewPrivate EWebViewPreviewPrivate; + +struct _EWebViewPreview { + GtkVPaned parent; + EWebViewPreviewPrivate *priv; +}; + +struct _EWebViewPreviewClass { + GtkVPanedClass parent_class; +}; + +GType e_web_view_preview_get_type (void); +GtkWidget * e_web_view_preview_new (void); +GtkTreeView * e_web_view_preview_get_tree_view + (EWebViewPreview *preview); +GtkWidget * e_web_view_preview_get_preview (EWebViewPreview *preview); +void e_web_view_preview_set_preview (EWebViewPreview *preview, + GtkWidget *preview_widget); +void e_web_view_preview_show_tree_view + (EWebViewPreview *preview); +void e_web_view_preview_hide_tree_view + (EWebViewPreview *preview); +void e_web_view_preview_set_escape_values + (EWebViewPreview *preview, + gboolean escape); +gboolean e_web_view_preview_get_escape_values + (EWebViewPreview *preview); +void e_web_view_preview_begin_update (EWebViewPreview *preview); +void e_web_view_preview_end_update (EWebViewPreview *preview); +void e_web_view_preview_add_header (EWebViewPreview *preview, + gint index, + const gchar *header); +void e_web_view_preview_add_text (EWebViewPreview *preview, + const gchar *text); +void e_web_view_preview_add_raw_html (EWebViewPreview *preview, + const gchar *raw_html); +void e_web_view_preview_add_separator + (EWebViewPreview *preview); +void e_web_view_preview_add_empty_line + (EWebViewPreview *preview); +void e_web_view_preview_add_section (EWebViewPreview *preview, + const gchar *section, + const gchar *value); + +G_END_DECLS + +#endif /* E_WEB_VIEW_PREVIEW_H */ diff --git a/e-util/e-web-view.c b/e-util/e-web-view.c new file mode 100644 index 0000000000..2fefe4fa95 --- /dev/null +++ b/e-util/e-web-view.c @@ -0,0 +1,2945 @@ +/* + * e-web-view.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-web-view.h" + +#include <math.h> + +#include <stdlib.h> +#include <string.h> +#include <glib/gi18n-lib.h> +#include <pango/pango.h> + +#include <camel/camel.h> +#include <libebackend/libebackend.h> + +#define LIBSOUP_USE_UNSTABLE_REQUEST_API +#include <libsoup/soup.h> +#include <libsoup/soup-requester.h> + +#include "e-alert-dialog.h" +#include "e-alert-sink.h" +#include "e-file-request.h" +#include "e-misc-utils.h" +#include "e-plugin-ui.h" +#include "e-popup-action.h" +#include "e-selectable.h" +#include "e-stock-request.h" + +#define E_WEB_VIEW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_WEB_VIEW, EWebViewPrivate)) + +typedef struct _EWebViewRequest EWebViewRequest; + +struct _EWebViewPrivate { + GList *requests; + GtkUIManager *ui_manager; + gchar *selected_uri; + GdkPixbufAnimation *cursor_image; + gchar *cursor_image_src; + + GSList *highlights; + + GtkAction *open_proxy; + GtkAction *print_proxy; + GtkAction *save_as_proxy; + + /* Lockdown Options */ + guint disable_printing : 1; + guint disable_save_to_disk : 1; + + guint caret_mode : 1; + + GSettings *font_settings; + GSettings *aliasing_settings; +}; + +enum { + PROP_0, + PROP_CARET_MODE, + PROP_COPY_TARGET_LIST, + PROP_CURSOR_IMAGE, + PROP_CURSOR_IMAGE_SRC, + PROP_DISABLE_PRINTING, + PROP_DISABLE_SAVE_TO_DISK, + PROP_INLINE_SPELLING, + PROP_MAGIC_LINKS, + PROP_MAGIC_SMILEYS, + PROP_OPEN_PROXY, + PROP_PRINT_PROXY, + PROP_SAVE_AS_PROXY, + PROP_SELECTED_URI +}; + +enum { + POPUP_EVENT, + STATUS_MESSAGE, + STOP_LOADING, + UPDATE_ACTIONS, + PROCESS_MAILTO, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +static const gchar *ui = +"<ui>" +" <popup name='context'>" +" <menuitem action='copy-clipboard'/>" +" <separator/>" +" <placeholder name='custom-actions-1'>" +" <menuitem action='open'/>" +" <menuitem action='save-as'/>" +" <menuitem action='http-open'/>" +" <menuitem action='send-message'/>" +" <menuitem action='print'/>" +" </placeholder>" +" <placeholder name='custom-actions-2'>" +" <menuitem action='uri-copy'/>" +" <menuitem action='mailto-copy'/>" +" <menuitem action='image-copy'/>" +" </placeholder>" +" <placeholder name='custom-actions-3'/>" +" <separator/>" +" <menuitem action='select-all'/>" +" <placeholder name='inspect-menu' />" +" </popup>" +"</ui>"; + +/* Forward Declarations */ +static void e_web_view_alert_sink_init (EAlertSinkInterface *interface); +static void e_web_view_selectable_init (ESelectableInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EWebView, + e_web_view, + WEBKIT_TYPE_WEB_VIEW, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL) + G_IMPLEMENT_INTERFACE ( + E_TYPE_ALERT_SINK, + e_web_view_alert_sink_init) + G_IMPLEMENT_INTERFACE ( + E_TYPE_SELECTABLE, + e_web_view_selectable_init)) + +static void +action_copy_clipboard_cb (GtkAction *action, + EWebView *web_view) +{ + e_web_view_copy_clipboard (web_view); +} + +static void +action_http_open_cb (GtkAction *action, + EWebView *web_view) +{ + const gchar *uri; + gpointer parent; + + parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + uri = e_web_view_get_selected_uri (web_view); + g_return_if_fail (uri != NULL); + + e_show_uri (parent, uri); +} + +static void +action_mailto_copy_cb (GtkAction *action, + EWebView *web_view) +{ + CamelURL *curl; + CamelInternetAddress *inet_addr; + GtkClipboard *clipboard; + const gchar *uri; + gchar *text; + + uri = e_web_view_get_selected_uri (web_view); + g_return_if_fail (uri != NULL); + + /* This should work because we checked it in update_actions(). */ + curl = camel_url_new (uri, NULL); + g_return_if_fail (curl != NULL); + + inet_addr = camel_internet_address_new (); + camel_address_decode (CAMEL_ADDRESS (inet_addr), curl->path); + text = camel_address_format (CAMEL_ADDRESS (inet_addr)); + if (text == NULL || *text == '\0') + text = g_strdup (uri + strlen ("mailto:")); + + g_object_unref (inet_addr); + camel_url_free (curl); + + clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); + gtk_clipboard_set_text (clipboard, text, -1); + gtk_clipboard_store (clipboard); + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_text (clipboard, text, -1); + gtk_clipboard_store (clipboard); + + g_free (text); +} + +static void +action_select_all_cb (GtkAction *action, + EWebView *web_view) +{ + e_web_view_select_all (web_view); +} + +static void +action_send_message_cb (GtkAction *action, + EWebView *web_view) +{ + const gchar *uri; + gpointer parent; + gboolean handled; + + parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + uri = e_web_view_get_selected_uri (web_view); + g_return_if_fail (uri != NULL); + + handled = FALSE; + g_signal_emit (web_view, signals[PROCESS_MAILTO], 0, uri, &handled); + + if (!handled) + e_show_uri (parent, uri); +} + +static void +action_uri_copy_cb (GtkAction *action, + EWebView *web_view) +{ + GtkClipboard *clipboard; + const gchar *uri; + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + uri = e_web_view_get_selected_uri (web_view); + g_return_if_fail (uri != NULL); + + gtk_clipboard_set_text (clipboard, uri, -1); + gtk_clipboard_store (clipboard); +} + +static void +action_image_copy_cb (GtkAction *action, + EWebView *web_view) +{ + GtkClipboard *clipboard; + GdkPixbufAnimation *animation; + GdkPixbuf *pixbuf; + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + animation = e_web_view_get_cursor_image (web_view); + g_return_if_fail (animation != NULL); + + pixbuf = gdk_pixbuf_animation_get_static_image (animation); + if (pixbuf == NULL) + return; + + gtk_clipboard_set_image (clipboard, pixbuf); + gtk_clipboard_store (clipboard); +} + +static GtkActionEntry uri_entries[] = { + + { "uri-copy", + GTK_STOCK_COPY, + N_("_Copy Link Location"), + NULL, + N_("Copy the link to the clipboard"), + G_CALLBACK (action_uri_copy_cb) } +}; + +static GtkActionEntry http_entries[] = { + + { "http-open", + "emblem-web", + N_("_Open Link in Browser"), + NULL, + N_("Open the link in a web browser"), + G_CALLBACK (action_http_open_cb) } +}; + +static GtkActionEntry mailto_entries[] = { + + { "mailto-copy", + GTK_STOCK_COPY, + N_("_Copy Email Address"), + NULL, + N_("Copy the email address to the clipboard"), + G_CALLBACK (action_mailto_copy_cb) }, + + { "send-message", + "mail-message-new", + N_("_Send New Message To..."), + NULL, + N_("Send a mail message to this address"), + G_CALLBACK (action_send_message_cb) } +}; + +static GtkActionEntry image_entries[] = { + + { "image-copy", + GTK_STOCK_COPY, + N_("_Copy Image"), + NULL, + N_("Copy the image to the clipboard"), + G_CALLBACK (action_image_copy_cb) } +}; + +static GtkActionEntry selection_entries[] = { + + { "copy-clipboard", + GTK_STOCK_COPY, + NULL, + NULL, + N_("Copy the selection"), + G_CALLBACK (action_copy_clipboard_cb) }, +}; + +static GtkActionEntry standard_entries[] = { + + { "select-all", + GTK_STOCK_SELECT_ALL, + NULL, + NULL, + N_("Select all text and images"), + G_CALLBACK (action_select_all_cb) } +}; + +static void +web_view_menu_item_select_cb (EWebView *web_view, + GtkWidget *widget) +{ + GtkAction *action; + GtkActivatable *activatable; + const gchar *tooltip; + + activatable = GTK_ACTIVATABLE (widget); + action = gtk_activatable_get_related_action (activatable); + tooltip = gtk_action_get_tooltip (action); + + if (tooltip == NULL) + return; + + e_web_view_status_message (web_view, tooltip); +} + +static void +replace_text (WebKitDOMNode *node, + const gchar *text, + WebKitDOMNode *replacement) +{ + /* NodeType 3 = TEXT_NODE */ + if (webkit_dom_node_get_node_type (node) == 3) { + gint text_length = strlen (text); + + while (node) { + + WebKitDOMNode *current_node, *replacement_node; + const gchar *node_data, *offset; + goffset split_offset; + gint data_length; + + current_node = node; + + /* Don't use the WEBKIT_DOM_CHARACTER_DATA macro for + * casting. WebKit lies about type of the object and + * GLib will throw runtime warning about node not being + * WebKitDOMCharacterData, but the function will return + * correct and valid data. + * IMO it's bug in the Gtk bindings and WebKit internally + * handles it by the nodeType so therefor it works + * event for "invalid" objects. But really, who knows..? + */ + node_data = webkit_dom_character_data_get_data ( + (WebKitDOMCharacterData *) node); + + offset = strstr (node_data, text); + if (offset == NULL) { + node = NULL; + continue; + } + + split_offset = offset - node_data + text_length; + replacement_node = + webkit_dom_node_clone_node (replacement, TRUE); + + data_length = webkit_dom_character_data_get_length ( + (WebKitDOMCharacterData *) node); + if (split_offset < data_length) { + + WebKitDOMNode *parent_node; + + node = WEBKIT_DOM_NODE ( + webkit_dom_text_split_text ( + (WebKitDOMText *) node, + offset - node_data + text_length, + NULL)); + parent_node = webkit_dom_node_get_parent_node (node); + webkit_dom_node_insert_before ( + parent_node, replacement_node, + node, NULL); + + } else { + WebKitDOMNode *parent_node; + + parent_node = webkit_dom_node_get_parent_node (node); + webkit_dom_node_append_child ( + parent_node, + replacement_node, NULL); + } + + webkit_dom_character_data_delete_data ( + (WebKitDOMCharacterData *) (current_node), + offset - node_data, text_length, NULL); + } + + } else { + WebKitDOMNode *child, *next_child; + + /* Iframe? Let's traverse inside! */ + if (WEBKIT_DOM_IS_HTML_IFRAME_ELEMENT (node)) { + + WebKitDOMDocument *frame_document; + + frame_document = + webkit_dom_html_iframe_element_get_content_document ( + WEBKIT_DOM_HTML_IFRAME_ELEMENT (node)); + replace_text ( + WEBKIT_DOM_NODE (frame_document), + text, replacement); + + } else { + child = webkit_dom_node_get_first_child (node); + while (child != NULL) { + next_child = webkit_dom_node_get_next_sibling (child); + replace_text (child, text, replacement); + child = next_child; + } + } + } +} + +static void +web_view_update_document_highlights (EWebView *web_view) +{ + WebKitDOMDocument *document; + GSList *iter; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (web_view)); + + for (iter = web_view->priv->highlights; iter; iter = iter->next) { + + WebKitDOMDocumentFragment *frag; + WebKitDOMElement *span; + + span = webkit_dom_document_create_element (document, "span", NULL); + + /* See https://bugzilla.gnome.org/show_bug.cgi?id=681400 + * FIXME: This can be removed once we require WebKitGtk 1.10+ */ + #if WEBKIT_CHECK_VERSION (1, 9, 6) + webkit_dom_element_set_class_name ( + span, "__evo-highlight"); + #else + webkit_dom_html_element_set_class_name ( + WEBKIT_DOM_HTML_ELEMENT (span), "__evo-highlight"); + #endif + + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (span), iter->data, NULL); + + frag = webkit_dom_document_create_document_fragment (document); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (frag), WEBKIT_DOM_NODE (span), NULL); + + replace_text ( + WEBKIT_DOM_NODE (document), + iter->data, WEBKIT_DOM_NODE (frag)); + } +} + +static void +web_view_menu_item_deselect_cb (EWebView *web_view) +{ + e_web_view_status_message (web_view, NULL); +} + +static void +web_view_connect_proxy_cb (EWebView *web_view, + GtkAction *action, + GtkWidget *proxy) +{ + if (!GTK_IS_MENU_ITEM (proxy)) + return; + + g_signal_connect_swapped ( + proxy, "select", + G_CALLBACK (web_view_menu_item_select_cb), web_view); + + g_signal_connect_swapped ( + proxy, "deselect", + G_CALLBACK (web_view_menu_item_deselect_cb), web_view); +} + +static GtkWidget * +web_view_create_plugin_widget_cb (EWebView *web_view, + const gchar *mime_type, + const gchar *uri, + GHashTable *param) +{ + EWebViewClass *class; + + /* XXX WebKitWebView does not provide a class method for + * this signal, so we do so we can override the default + * behavior from subclasses for special URI types. */ + + class = E_WEB_VIEW_GET_CLASS (web_view); + g_return_val_if_fail (class->create_plugin_widget != NULL, NULL); + + return class->create_plugin_widget (web_view, mime_type, uri, param); +} + +static void +web_view_hovering_over_link_cb (EWebView *web_view, + const gchar *title, + const gchar *uri) +{ + EWebViewClass *class; + + /* XXX WebKitWebView does not provide a class method for + * this signal, so we do so we can override the default + * behavior from subclasses for special URI types. */ + + class = E_WEB_VIEW_GET_CLASS (web_view); + g_return_if_fail (class->hovering_over_link != NULL); + + class->hovering_over_link (web_view, title, uri); +} + +static gboolean +web_view_navigation_policy_decision_requested_cb (EWebView *web_view, + WebKitWebFrame *frame, + WebKitNetworkRequest *request, + WebKitWebNavigationAction *navigation_action, + WebKitWebPolicyDecision *policy_decision) +{ + EWebViewClass *class; + WebKitWebNavigationReason reason; + const gchar *uri; + + reason = webkit_web_navigation_action_get_reason (navigation_action); + if (reason != WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) + return FALSE; + + /* XXX WebKitWebView does not provide a class method for + * this signal, so we do so we can override the default + * behavior from subclasses for special URI types. */ + + class = E_WEB_VIEW_GET_CLASS (web_view); + g_return_val_if_fail (class->link_clicked != NULL, FALSE); + + webkit_web_policy_decision_ignore (policy_decision); + + uri = webkit_network_request_get_uri (request); + + class->link_clicked (web_view, uri); + + return TRUE; +} + +static void +web_view_load_status_changed_cb (WebKitWebView *webkit_web_view, + GParamSpec *pspec, + gpointer user_data) +{ + WebKitLoadStatus status; + EWebView *web_view; + + status = webkit_web_view_get_load_status (webkit_web_view); + if (status != WEBKIT_LOAD_FINISHED) + return; + + web_view = E_WEB_VIEW (webkit_web_view); + web_view_update_document_highlights (web_view); + + /* Workaround webkit bug https://bugs.webkit.org/show_bug.cgi?id=89553 */ + e_web_view_zoom_in (web_view); + e_web_view_zoom_out (web_view); +} + +static void +web_view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CARET_MODE: + e_web_view_set_caret_mode ( + E_WEB_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_CURSOR_IMAGE: + e_web_view_set_cursor_image ( + E_WEB_VIEW (object), + g_value_get_object (value)); + return; + + case PROP_CURSOR_IMAGE_SRC: + e_web_view_set_cursor_image_src ( + E_WEB_VIEW (object), + g_value_get_string (value)); + return; + + case PROP_DISABLE_PRINTING: + e_web_view_set_disable_printing ( + E_WEB_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_DISABLE_SAVE_TO_DISK: + e_web_view_set_disable_save_to_disk ( + E_WEB_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_INLINE_SPELLING: + e_web_view_set_inline_spelling ( + E_WEB_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_MAGIC_LINKS: + e_web_view_set_magic_links ( + E_WEB_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_MAGIC_SMILEYS: + e_web_view_set_magic_smileys ( + E_WEB_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_OPEN_PROXY: + e_web_view_set_open_proxy ( + E_WEB_VIEW (object), + g_value_get_object (value)); + return; + + case PROP_PRINT_PROXY: + e_web_view_set_print_proxy ( + E_WEB_VIEW (object), + g_value_get_object (value)); + return; + + case PROP_SAVE_AS_PROXY: + e_web_view_set_save_as_proxy ( + E_WEB_VIEW (object), + g_value_get_object (value)); + return; + + case PROP_SELECTED_URI: + e_web_view_set_selected_uri ( + E_WEB_VIEW (object), + g_value_get_string (value)); + return; + } + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +web_view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CARET_MODE: + g_value_set_boolean ( + value, e_web_view_get_caret_mode ( + E_WEB_VIEW (object))); + return; + + case PROP_CURSOR_IMAGE: + g_value_set_object ( + value, e_web_view_get_cursor_image ( + E_WEB_VIEW (object))); + return; + + case PROP_CURSOR_IMAGE_SRC: + g_value_set_string ( + value, e_web_view_get_cursor_image_src ( + E_WEB_VIEW (object))); + return; + + case PROP_DISABLE_PRINTING: + g_value_set_boolean ( + value, e_web_view_get_disable_printing ( + E_WEB_VIEW (object))); + return; + + case PROP_DISABLE_SAVE_TO_DISK: + g_value_set_boolean ( + value, e_web_view_get_disable_save_to_disk ( + E_WEB_VIEW (object))); + return; + + case PROP_INLINE_SPELLING: + g_value_set_boolean ( + value, e_web_view_get_inline_spelling ( + E_WEB_VIEW (object))); + return; + + case PROP_MAGIC_LINKS: + g_value_set_boolean ( + value, e_web_view_get_magic_links ( + E_WEB_VIEW (object))); + return; + + case PROP_MAGIC_SMILEYS: + g_value_set_boolean ( + value, e_web_view_get_magic_smileys ( + E_WEB_VIEW (object))); + return; + + case PROP_OPEN_PROXY: + g_value_set_object ( + value, e_web_view_get_open_proxy ( + E_WEB_VIEW (object))); + return; + + case PROP_PRINT_PROXY: + g_value_set_object ( + value, e_web_view_get_print_proxy ( + E_WEB_VIEW (object))); + return; + + case PROP_SAVE_AS_PROXY: + g_value_set_object ( + value, e_web_view_get_save_as_proxy ( + E_WEB_VIEW (object))); + return; + + case PROP_SELECTED_URI: + g_value_set_string ( + value, e_web_view_get_selected_uri ( + E_WEB_VIEW (object))); + return; + + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +web_view_dispose (GObject *object) +{ + EWebViewPrivate *priv; + + priv = E_WEB_VIEW_GET_PRIVATE (object); + + if (priv->ui_manager != NULL) { + g_object_unref (priv->ui_manager); + priv->ui_manager = NULL; + } + + if (priv->open_proxy != NULL) { + g_object_unref (priv->open_proxy); + priv->open_proxy = NULL; + } + + if (priv->print_proxy != NULL) { + g_object_unref (priv->print_proxy); + priv->print_proxy = NULL; + } + + if (priv->save_as_proxy != NULL) { + g_object_unref (priv->save_as_proxy); + priv->save_as_proxy = NULL; + } + + if (priv->cursor_image != NULL) { + g_object_unref (priv->cursor_image); + priv->cursor_image = NULL; + } + + if (priv->cursor_image_src != NULL) { + g_free (priv->cursor_image_src); + priv->cursor_image_src = NULL; + } + + if (priv->highlights != NULL) { + g_slist_free_full (priv->highlights, g_free); + priv->highlights = NULL; + } + + if (priv->aliasing_settings != NULL) { + g_signal_handlers_disconnect_matched ( + priv->aliasing_settings, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->aliasing_settings); + priv->aliasing_settings = NULL; + } + + if (priv->font_settings != NULL) { + g_signal_handlers_disconnect_matched ( + priv->font_settings, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->font_settings); + priv->font_settings = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_web_view_parent_class)->dispose (object); +} + +static void +web_view_finalize (GObject *object) +{ + EWebViewPrivate *priv; + + priv = E_WEB_VIEW_GET_PRIVATE (object); + + /* All URI requests should be complete or cancelled by now. */ + if (priv->requests != NULL) + g_warning ("Finalizing EWebView with active URI requests"); + + g_free (priv->selected_uri); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_web_view_parent_class)->finalize (object); +} + +static void +web_view_constructed (GObject *object) +{ +#ifndef G_OS_WIN32 + GSettings *settings; + + settings = g_settings_new ("org.gnome.desktop.lockdown"); + + g_settings_bind ( + settings, "disable-printing", + object, "disable-printing", + G_SETTINGS_BIND_GET); + + g_settings_bind ( + settings, "disable-save-to-disk", + object, "disable-save-to-disk", + G_SETTINGS_BIND_GET); + + g_object_unref (settings); +#endif + + e_extensible_load_extensions (E_EXTENSIBLE (object)); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_web_view_parent_class)->constructed (object); +} + +static gboolean +web_view_context_menu_cb (WebKitWebView *webkit_web_view, + GtkWidget *default_menu, + WebKitHitTestResult *hit_test_result, + gboolean triggered_with_keyboard) +{ + WebKitHitTestResultContext context; + EWebView *web_view; + gboolean event_handled = FALSE; + gchar *uri; + + web_view = E_WEB_VIEW (webkit_web_view); + + if (web_view->priv->cursor_image != NULL) { + g_object_unref (web_view->priv->cursor_image); + web_view->priv->cursor_image = NULL; + } + + if (web_view->priv->cursor_image_src != NULL) { + g_free (web_view->priv->cursor_image_src); + web_view->priv->cursor_image_src = NULL; + } + + if (hit_test_result == NULL) + return FALSE; + + g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL); + + if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE) { + WebKitWebDataSource *data_source; + WebKitWebFrame *frame; + GList *subresources, *res; + + g_object_get ( + G_OBJECT (hit_test_result), "image-uri", &uri, NULL); + + if (uri == NULL) + return FALSE; + + g_free (web_view->priv->cursor_image_src); + web_view->priv->cursor_image_src = uri; + + /* Iterate through all resources of the loaded webpage and + * try to find resource with URI matching cursor_image_src */ + frame = webkit_web_view_get_main_frame (WEBKIT_WEB_VIEW (web_view)); + data_source = webkit_web_frame_get_data_source (frame); + subresources = webkit_web_data_source_get_subresources (data_source); + for (res = subresources; res; res = res->next) { + WebKitWebResource *src = res->data; + GdkPixbufLoader *loader; + GString *data; + + if (g_strcmp0 (webkit_web_resource_get_uri (src), + web_view->priv->cursor_image_src) != 0) + continue; + + data = webkit_web_resource_get_data (src); + if (data == NULL) + break; + + loader = gdk_pixbuf_loader_new (); + if (!gdk_pixbuf_loader_write (loader, + (guchar *) data->str, data->len, NULL)) { + g_object_unref (loader); + break; + } + gdk_pixbuf_loader_close (loader, NULL); + + if (web_view->priv->cursor_image != NULL) + g_object_unref (web_view->priv->cursor_image); + + web_view->priv->cursor_image = + g_object_ref (gdk_pixbuf_loader_get_animation (loader)); + + g_object_unref (loader); + break; + } + g_list_free (subresources); + } + + g_object_get (hit_test_result, "link-uri", &uri, NULL); + + if (!(context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK)) { + g_free (uri); + uri = NULL; + } + + g_signal_emit (web_view, signals[POPUP_EVENT], 0, uri, &event_handled); + + g_free (uri); + + return event_handled; +} + +static gboolean +web_view_scroll_event (GtkWidget *widget, + GdkEventScroll *event) +{ + if (event->state & GDK_CONTROL_MASK) { + GdkScrollDirection direction = event->direction; + + if (direction == GDK_SCROLL_SMOOTH) { + static gdouble total_delta_y = 0.0; + + total_delta_y += event->delta_y; + + if (total_delta_y >= 1.0) { + total_delta_y = 0.0; + direction = GDK_SCROLL_DOWN; + } else if (total_delta_y <= -1.0) { + total_delta_y = 0.0; + direction = GDK_SCROLL_UP; + } else { + return FALSE; + } + } + + switch (direction) { + case GDK_SCROLL_UP: + e_web_view_zoom_in (E_WEB_VIEW (widget)); + return TRUE; + case GDK_SCROLL_DOWN: + e_web_view_zoom_out (E_WEB_VIEW (widget)); + return TRUE; + default: + break; + } + } + + return FALSE; +} + +static GtkWidget * +web_view_create_plugin_widget (EWebView *web_view, + const gchar *mime_type, + const gchar *uri, + GHashTable *param) +{ + GtkWidget *widget = NULL; + + if (g_strcmp0 (mime_type, "image/x-themed-icon") == 0) { + GtkIconTheme *icon_theme; + GdkPixbuf *pixbuf; + gpointer data; + glong size = 0; + GError *error = NULL; + + icon_theme = gtk_icon_theme_get_default (); + + if (size == 0) { + data = g_hash_table_lookup (param, "width"); + if (data != NULL) + size = MAX (size, strtol (data, NULL, 10)); + } + + if (size == 0) { + data = g_hash_table_lookup (param, "height"); + if (data != NULL) + size = MAX (size, strtol (data, NULL, 10)); + } + + if (size == 0) + size = 32; /* arbitrary default */ + + pixbuf = gtk_icon_theme_load_icon ( + icon_theme, uri, size, 0, &error); + if (pixbuf != NULL) { + widget = gtk_image_new_from_pixbuf (pixbuf); + g_object_unref (pixbuf); + } else if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } + } + + return widget; +} + +static gchar * +web_view_extract_uri (EWebView *web_view, + GdkEventButton *event) +{ + WebKitHitTestResult *result; + WebKitHitTestResultContext context; + gchar *uri = NULL; + + result = webkit_web_view_get_hit_test_result ( + WEBKIT_WEB_VIEW (web_view), event); + + g_object_get (result, "context", &context, "link-uri", &uri, NULL); + g_object_unref (result); + + if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) + return uri; + + g_free (uri); + + return NULL; +} + +static void +web_view_hovering_over_link (EWebView *web_view, + const gchar *title, + const gchar *uri) +{ + CamelInternetAddress *address; + CamelURL *curl; + const gchar *format = NULL; + gchar *message = NULL; + gchar *who; + + if (uri == NULL || *uri == '\0') + goto exit; + + if (g_str_has_prefix (uri, "mailto:")) + format = _("Click to mail %s"); + else if (g_str_has_prefix (uri, "callto:")) + format = _("Click to call %s"); + else if (g_str_has_prefix (uri, "h323:")) + format = _("Click to call %s"); + else if (g_str_has_prefix (uri, "sip:")) + format = _("Click to call %s"); + else if (g_str_has_prefix (uri, "##")) + message = g_strdup (_("Click to hide/unhide addresses")); + else + message = g_strdup_printf (_("Click to open %s"), uri); + + if (format == NULL) + goto exit; + + /* XXX Use something other than Camel here. Surely + * there's other APIs around that can do this. */ + curl = camel_url_new (uri, NULL); + address = camel_internet_address_new (); + camel_address_decode (CAMEL_ADDRESS (address), curl->path); + who = camel_address_format (CAMEL_ADDRESS (address)); + g_object_unref (address); + camel_url_free (curl); + + if (who == NULL) + who = g_strdup (strchr (uri, ':') + 1); + + message = g_strdup_printf (format, who); + + g_free (who); + +exit: + e_web_view_status_message (web_view, message); + + g_free (message); +} + +static void +web_view_link_clicked (EWebView *web_view, + const gchar *uri) +{ + gpointer parent; + + if (uri && g_ascii_strncasecmp (uri, "mailto:", 7) == 0) { + gboolean handled = FALSE; + + g_signal_emit ( + web_view, signals[PROCESS_MAILTO], 0, uri, &handled); + + if (handled) + return; + } + + parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + e_show_uri (parent, uri); +} + +static void +web_view_load_string (EWebView *web_view, + const gchar *string) +{ + if (string == NULL) + string = ""; + + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (web_view), + string, "text/html", "UTF-8", "evo-file:///"); +} + +static void +web_view_load_uri (EWebView *web_view, + const gchar *uri) +{ + if (uri == NULL) + uri = "about:blank"; + + webkit_web_view_load_uri (WEBKIT_WEB_VIEW (web_view), uri); +} + +static void +web_view_frame_load_string (EWebView *web_view, + const gchar *frame_name, + const gchar *string) +{ + WebKitWebFrame *main_frame; + + if (string == NULL) + string = ""; + + main_frame = webkit_web_view_get_main_frame (WEBKIT_WEB_VIEW (web_view)); + if (main_frame != NULL) { + WebKitWebFrame *frame; + + frame = webkit_web_frame_find_frame (main_frame, frame_name); + + if (frame != NULL) + webkit_web_frame_load_string ( + frame, string, "text/html", + "UTF-8", "evo-file:///"); + } +} + +static void +web_view_frame_load_uri (EWebView *web_view, + const gchar *frame_name, + const gchar *uri) +{ + WebKitWebFrame *main_frame; + + if (uri == NULL) + uri = "about:blank"; + + main_frame = webkit_web_view_get_main_frame (WEBKIT_WEB_VIEW (web_view)); + if (main_frame != NULL) { + WebKitWebFrame *frame; + + frame = webkit_web_frame_find_frame (main_frame, frame_name); + + if (frame != NULL) + webkit_web_frame_load_uri (frame, uri); + } +} + +static gboolean +web_view_popup_event (EWebView *web_view, + const gchar *uri) +{ + e_web_view_set_selected_uri (web_view, uri); + e_web_view_show_popup_menu (web_view); + + return TRUE; +} + +static void +web_view_stop_loading (EWebView *web_view) +{ + webkit_web_view_stop_loading (WEBKIT_WEB_VIEW (web_view)); +} + +static void +web_view_update_actions (EWebView *web_view) +{ + GtkActionGroup *action_group; + gboolean can_copy; + gboolean scheme_is_http = FALSE; + gboolean scheme_is_mailto = FALSE; + gboolean uri_is_valid = FALSE; + gboolean has_cursor_image; + gboolean visible; + const gchar *group_name; + const gchar *uri; + + uri = e_web_view_get_selected_uri (web_view); + can_copy = webkit_web_view_can_copy_clipboard (WEBKIT_WEB_VIEW (web_view)); + has_cursor_image = e_web_view_get_cursor_image_src (web_view) || + e_web_view_get_cursor_image (web_view); + + /* Parse the URI early so we know if the actions will work. */ + if (uri != NULL) { + CamelURL *curl; + + curl = camel_url_new (uri, NULL); + uri_is_valid = (curl != NULL); + camel_url_free (curl); + + scheme_is_http = + (g_ascii_strncasecmp (uri, "http:", 5) == 0) || + (g_ascii_strncasecmp (uri, "https:", 6) == 0); + + scheme_is_mailto = + (g_ascii_strncasecmp (uri, "mailto:", 7) == 0); + } + + /* Allow copying the URI even if it's malformed. */ + group_name = "uri"; + visible = (uri != NULL) && !scheme_is_mailto; + action_group = e_web_view_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "http"; + visible = uri_is_valid && scheme_is_http; + action_group = e_web_view_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "mailto"; + visible = uri_is_valid && scheme_is_mailto; + action_group = e_web_view_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "image"; + visible = has_cursor_image; + action_group = e_web_view_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "selection"; + visible = can_copy; + action_group = e_web_view_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "standard"; + visible = (uri == NULL); + action_group = e_web_view_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "lockdown-printing"; + visible = (uri == NULL) && !web_view->priv->disable_printing; + action_group = e_web_view_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); + + group_name = "lockdown-save-to-disk"; + visible = (uri == NULL) && !web_view->priv->disable_save_to_disk; + action_group = e_web_view_get_action_group (web_view, group_name); + gtk_action_group_set_visible (action_group, visible); +} + +static void +web_view_submit_alert (EAlertSink *alert_sink, + EAlert *alert) +{ + EWebView *web_view; + GtkWidget *dialog; + GString *buffer; + const gchar *icon_name = NULL; + gpointer parent; + + web_view = E_WEB_VIEW (alert_sink); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + switch (e_alert_get_message_type (alert)) { + case GTK_MESSAGE_INFO: + icon_name = GTK_STOCK_DIALOG_INFO; + break; + + case GTK_MESSAGE_WARNING: + icon_name = GTK_STOCK_DIALOG_WARNING; + break; + + case GTK_MESSAGE_ERROR: + icon_name = GTK_STOCK_DIALOG_ERROR; + break; + + default: + dialog = e_alert_dialog_new (parent, alert); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + return; + } + + buffer = g_string_sized_new (512); + + g_string_append ( + buffer, + "<html>" + "<head>" + "<meta http-equiv=\"content-type\"" + " content=\"text/html; charset=utf-8\">" + "</head>" + "<body>"); + + g_string_append ( + buffer, + "<table bgcolor='#000000' width='100%'" + " cellpadding='1' cellspacing='0'>" + "<tr>" + "<td>" + "<table bgcolor='#dddddd' width='100%' cellpadding='6'>" + "<tr>"); + + g_string_append_printf ( + buffer, + "<tr>" + "<td valign='top'>" + "<img src='gtk-stock://%s/?size=%d'/>" + "</td>" + "<td align='left' width='100%%'>" + "<h3>%s</h3>" + "%s" + "</td>" + "</tr>", + icon_name, + GTK_ICON_SIZE_DIALOG, + e_alert_get_primary_text (alert), + e_alert_get_secondary_text (alert)); + + g_string_append ( + buffer, + "</table>" + "</td>" + "</tr>" + "</table>" + "</body>" + "</html>"); + + e_web_view_load_string (web_view, buffer->str); + + g_string_free (buffer, TRUE); +} + +static void +web_view_selectable_update_actions (ESelectable *selectable, + EFocusTracker *focus_tracker, + GdkAtom *clipboard_targets, + gint n_clipboard_targets) +{ + WebKitWebView *web_view; + GtkAction *action; + gboolean sensitive; + const gchar *tooltip; + + web_view = WEBKIT_WEB_VIEW (selectable); + + action = e_focus_tracker_get_cut_clipboard_action (focus_tracker); + sensitive = webkit_web_view_can_cut_clipboard (web_view); + tooltip = _("Cut the selection"); + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, tooltip); + + action = e_focus_tracker_get_copy_clipboard_action (focus_tracker); + sensitive = webkit_web_view_can_copy_clipboard (web_view); + tooltip = _("Copy the selection"); + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, tooltip); + + action = e_focus_tracker_get_paste_clipboard_action (focus_tracker); + sensitive = webkit_web_view_can_paste_clipboard (web_view); + tooltip = _("Paste the clipboard"); + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, tooltip); + + action = e_focus_tracker_get_select_all_action (focus_tracker); + sensitive = TRUE; + tooltip = _("Select all text and images"); + gtk_action_set_sensitive (action, sensitive); + gtk_action_set_tooltip (action, tooltip); +} + +static void +web_view_selectable_cut_clipboard (ESelectable *selectable) +{ + e_web_view_cut_clipboard (E_WEB_VIEW (selectable)); +} + +static void +web_view_selectable_copy_clipboard (ESelectable *selectable) +{ + e_web_view_copy_clipboard (E_WEB_VIEW (selectable)); +} + +static void +web_view_selectable_paste_clipboard (ESelectable *selectable) +{ + e_web_view_paste_clipboard (E_WEB_VIEW (selectable)); +} + +static void +web_view_selectable_select_all (ESelectable *selectable) +{ + e_web_view_select_all (E_WEB_VIEW (selectable)); +} + +static gboolean +web_view_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time_) +{ + return FALSE; +} + +static void +e_web_view_class_init (EWebViewClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EWebViewPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = web_view_set_property; + object_class->get_property = web_view_get_property; + object_class->dispose = web_view_dispose; + object_class->finalize = web_view_finalize; + object_class->constructed = web_view_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->scroll_event = web_view_scroll_event; + widget_class->drag_motion = web_view_drag_motion; + +#if 0 /* WEBKIT */ + html_class = GTK_HTML_CLASS (class); + html_class->url_requested = web_view_url_requested; +#endif + + class->create_plugin_widget = web_view_create_plugin_widget; + class->extract_uri = web_view_extract_uri; + class->hovering_over_link = web_view_hovering_over_link; + class->link_clicked = web_view_link_clicked; + class->load_string = web_view_load_string; + class->load_uri = web_view_load_uri; + class->frame_load_string = web_view_frame_load_string; + class->frame_load_uri = web_view_frame_load_uri; + class->popup_event = web_view_popup_event; + class->stop_loading = web_view_stop_loading; + class->update_actions = web_view_update_actions; + + g_object_class_install_property ( + object_class, + PROP_CARET_MODE, + g_param_spec_boolean ( + "caret-mode", + "Caret Mode", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_IMAGE, + g_param_spec_object ( + "cursor-image", + "Image animation at the mouse cursor", + NULL, + GDK_TYPE_PIXBUF_ANIMATION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_CURSOR_IMAGE_SRC, + g_param_spec_string ( + "cursor-image-src", + "Image source uri at the mouse cursor", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_DISABLE_PRINTING, + g_param_spec_boolean ( + "disable-printing", + "Disable Printing", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_DISABLE_SAVE_TO_DISK, + g_param_spec_boolean ( + "disable-save-to-disk", + "Disable Save-to-Disk", + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property ( + object_class, + PROP_INLINE_SPELLING, + g_param_spec_boolean ( + "inline-spelling", + "Inline Spelling", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MAGIC_LINKS, + g_param_spec_boolean ( + "magic-links", + "Magic Links", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_MAGIC_SMILEYS, + g_param_spec_boolean ( + "magic-smileys", + "Magic Smileys", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_OPEN_PROXY, + g_param_spec_object ( + "open-proxy", + "Open Proxy", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_PRINT_PROXY, + g_param_spec_object ( + "print-proxy", + "Print Proxy", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SAVE_AS_PROXY, + g_param_spec_object ( + "save-as-proxy", + "Save As Proxy", + NULL, + GTK_TYPE_ACTION, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_SELECTED_URI, + g_param_spec_string ( + "selected-uri", + "Selected URI", + NULL, + NULL, + G_PARAM_READWRITE)); + + signals[POPUP_EVENT] = g_signal_new ( + "popup-event", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewClass, popup_event), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__STRING, + G_TYPE_BOOLEAN, 1, G_TYPE_STRING); + + signals[STATUS_MESSAGE] = g_signal_new ( + "status-message", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewClass, status_message), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + signals[STOP_LOADING] = g_signal_new ( + "stop-loading", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewClass, stop_loading), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[UPDATE_ACTIONS] = g_signal_new ( + "update-actions", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewClass, update_actions), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /* return TRUE when a signal handler processed the mailto URI */ + signals[PROCESS_MAILTO] = g_signal_new ( + "process-mailto", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EWebViewClass, process_mailto), + NULL, NULL, + e_marshal_BOOLEAN__STRING, + G_TYPE_BOOLEAN, 1, G_TYPE_STRING); +} + +static void +e_web_view_alert_sink_init (EAlertSinkInterface *interface) +{ + interface->submit_alert = web_view_submit_alert; +} + +static void +e_web_view_selectable_init (ESelectableInterface *interface) +{ + interface->update_actions = web_view_selectable_update_actions; + interface->cut_clipboard = web_view_selectable_cut_clipboard; + interface->copy_clipboard = web_view_selectable_copy_clipboard; + interface->paste_clipboard = web_view_selectable_paste_clipboard; + interface->select_all = web_view_selectable_select_all; +} + +static void +e_web_view_init (EWebView *web_view) +{ + GtkUIManager *ui_manager; + GtkActionGroup *action_group; + EPopupAction *popup_action; + WebKitWebSettings *web_settings; + GSettingsSchema *settings_schema; + GSettings *settings; + const gchar *domain = GETTEXT_PACKAGE; + const gchar *id; + GError *error = NULL; + + web_view->priv = E_WEB_VIEW_GET_PRIVATE (web_view); + + web_view->priv->highlights = NULL; + + g_signal_connect ( + web_view, "create-plugin-widget", + G_CALLBACK (web_view_create_plugin_widget_cb), NULL); + + g_signal_connect ( + web_view, "hovering-over-link", + G_CALLBACK (web_view_hovering_over_link_cb), NULL); + + g_signal_connect ( + web_view, "navigation-policy-decision-requested", + G_CALLBACK (web_view_navigation_policy_decision_requested_cb), + NULL); + + g_signal_connect ( + web_view, "new-window-policy-decision-requested", + G_CALLBACK (web_view_navigation_policy_decision_requested_cb), + NULL); + + g_signal_connect ( + web_view, "context-menu", + G_CALLBACK (web_view_context_menu_cb), NULL); + + g_signal_connect ( + web_view, "notify::load-status", + G_CALLBACK (web_view_load_status_changed_cb), NULL); + + ui_manager = gtk_ui_manager_new (); + web_view->priv->ui_manager = ui_manager; + + g_signal_connect_swapped ( + ui_manager, "connect-proxy", + G_CALLBACK (web_view_connect_proxy_cb), web_view); + + web_settings = e_web_view_get_default_settings (); + e_web_view_set_settings (web_view, web_settings); + g_object_unref (web_settings); + + e_web_view_install_request_handler (web_view, E_TYPE_FILE_REQUEST); + e_web_view_install_request_handler (web_view, E_TYPE_STOCK_REQUEST); + + settings = g_settings_new ("org.gnome.desktop.interface"); + g_signal_connect_swapped ( + settings, "changed::font-name", + G_CALLBACK (e_web_view_update_fonts), web_view); + g_signal_connect_swapped ( + settings, "changed::monospace-font-name", + G_CALLBACK (e_web_view_update_fonts), web_view); + web_view->priv->font_settings = settings; + + /* This schema is optional. Use if available. */ + id = "org.gnome.settings-daemon.plugins.xsettings"; + settings_schema = g_settings_schema_source_lookup ( + g_settings_schema_source_get_default (), id, FALSE); + if (settings_schema != NULL) { + settings = g_settings_new (id); + g_signal_connect_swapped ( + settings, "changed::antialiasing", + G_CALLBACK (e_web_view_update_fonts), web_view); + web_view->priv->aliasing_settings = settings; + } + + e_web_view_update_fonts (web_view); + + action_group = gtk_action_group_new ("uri"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, uri_entries, + G_N_ELEMENTS (uri_entries), web_view); + + action_group = gtk_action_group_new ("http"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, http_entries, + G_N_ELEMENTS (http_entries), web_view); + + action_group = gtk_action_group_new ("mailto"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, mailto_entries, + G_N_ELEMENTS (mailto_entries), web_view); + + action_group = gtk_action_group_new ("image"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, image_entries, + G_N_ELEMENTS (image_entries), web_view); + + action_group = gtk_action_group_new ("selection"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, selection_entries, + G_N_ELEMENTS (selection_entries), web_view); + + action_group = gtk_action_group_new ("standard"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + gtk_action_group_add_actions ( + action_group, standard_entries, + G_N_ELEMENTS (standard_entries), web_view); + + popup_action = e_popup_action_new ("open"); + gtk_action_group_add_action (action_group, GTK_ACTION (popup_action)); + g_object_unref (popup_action); + + g_object_bind_property ( + web_view, "open-proxy", + popup_action, "related-action", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + /* Support lockdown. */ + + action_group = gtk_action_group_new ("lockdown-printing"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + popup_action = e_popup_action_new ("print"); + gtk_action_group_add_action (action_group, GTK_ACTION (popup_action)); + g_object_unref (popup_action); + + g_object_bind_property ( + web_view, "print-proxy", + popup_action, "related-action", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + action_group = gtk_action_group_new ("lockdown-save-to-disk"); + gtk_action_group_set_translation_domain (action_group, domain); + gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); + g_object_unref (action_group); + + popup_action = e_popup_action_new ("save-as"); + gtk_action_group_add_action (action_group, GTK_ACTION (popup_action)); + g_object_unref (popup_action); + + g_object_bind_property ( + web_view, "save-as-proxy", + popup_action, "related-action", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + /* Because we are loading from a hard-coded string, there is + * no chance of I/O errors. Failure here implies a malformed + * UI definition. Full stop. */ + gtk_ui_manager_add_ui_from_string (ui_manager, ui, -1, &error); + if (error != NULL) + g_error ("%s", error->message); + + id = "org.gnome.evolution.webview"; + e_plugin_ui_register_manager (ui_manager, id, web_view); + e_plugin_ui_enable_manager (ui_manager, id); +} + +GtkWidget * +e_web_view_new (void) +{ + return g_object_new (E_TYPE_WEB_VIEW, NULL); +} + +void +e_web_view_clear (EWebView *web_view) +{ + GtkStyle *style; + gchar *html; + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + style = gtk_widget_get_style (GTK_WIDGET (web_view)); + + html = g_strdup_printf ( + "<html><head></head><body bgcolor=\"#%06x\"></body></html>", + e_color_to_value (&style->base[GTK_STATE_NORMAL])); + + webkit_web_view_load_html_string ( + WEBKIT_WEB_VIEW (web_view), html, NULL); + + g_free (html); +} + +void +e_web_view_load_string (EWebView *web_view, + const gchar *string) +{ + EWebViewClass *class; + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + class = E_WEB_VIEW_GET_CLASS (web_view); + g_return_if_fail (class->load_string != NULL); + + class->load_string (web_view, string); +} + +void +e_web_view_load_uri (EWebView *web_view, + const gchar *uri) +{ + EWebViewClass *class; + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + class = E_WEB_VIEW_GET_CLASS (web_view); + g_return_if_fail (class->load_uri != NULL); + + class->load_uri (web_view, uri); +} + +void +e_web_view_reload (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + webkit_web_view_reload (WEBKIT_WEB_VIEW (web_view)); +} + +const gchar * +e_web_view_get_uri (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return webkit_web_view_get_uri (WEBKIT_WEB_VIEW (web_view)); +} + +void +e_web_view_frame_load_string (EWebView *web_view, + const gchar *frame_name, + const gchar *string) +{ + EWebViewClass *class; + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + g_return_if_fail (frame_name != NULL); + + class = E_WEB_VIEW_GET_CLASS (web_view); + g_return_if_fail (class->frame_load_string != NULL); + + class->frame_load_string (web_view, frame_name, string); +} + +void +e_web_view_frame_load_uri (EWebView *web_view, + const gchar *frame_name, + const gchar *uri) +{ + EWebViewClass *class; + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + g_return_if_fail (frame_name != NULL); + + class = E_WEB_VIEW_GET_CLASS (web_view); + g_return_if_fail (class->frame_load_uri != NULL); + + class->frame_load_uri (web_view, frame_name, uri); +} + +const gchar * +e_web_view_frame_get_uri (EWebView *web_view, + const gchar *frame_name) +{ + WebKitWebFrame *main_frame; + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + g_return_val_if_fail (frame_name != NULL, NULL); + + main_frame = webkit_web_view_get_main_frame (WEBKIT_WEB_VIEW (web_view)); + if (main_frame != NULL) { + WebKitWebFrame *frame; + + frame = webkit_web_frame_find_frame (main_frame, frame_name); + + if (frame != NULL) + return webkit_web_frame_get_uri (frame); + } + + return NULL; +} + +gchar * +e_web_view_get_html (EWebView *web_view) +{ + WebKitDOMDocument *document; + WebKitDOMElement *element; + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (web_view)); + element = webkit_dom_document_get_document_element (document); + + return webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); +} + +gboolean +e_web_view_get_caret_mode (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return web_view->priv->caret_mode; +} + +void +e_web_view_set_caret_mode (EWebView *web_view, + gboolean caret_mode) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (web_view->priv->caret_mode == caret_mode) + return; + + web_view->priv->caret_mode = caret_mode; + + g_object_notify (G_OBJECT (web_view), "caret-mode"); +} + +GtkTargetList * +e_web_view_get_copy_target_list (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return webkit_web_view_get_copy_target_list ( + WEBKIT_WEB_VIEW (web_view)); +} + +gboolean +e_web_view_get_disable_printing (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return web_view->priv->disable_printing; +} + +void +e_web_view_set_disable_printing (EWebView *web_view, + gboolean disable_printing) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (web_view->priv->disable_printing == disable_printing) + return; + + web_view->priv->disable_printing = disable_printing; + + g_object_notify (G_OBJECT (web_view), "disable-printing"); +} + +gboolean +e_web_view_get_disable_save_to_disk (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return web_view->priv->disable_save_to_disk; +} + +void +e_web_view_set_disable_save_to_disk (EWebView *web_view, + gboolean disable_save_to_disk) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (web_view->priv->disable_save_to_disk == disable_save_to_disk) + return; + + web_view->priv->disable_save_to_disk = disable_save_to_disk; + + g_object_notify (G_OBJECT (web_view), "disable-save-to-disk"); +} + +gboolean +e_web_view_get_enable_frame_flattening (EWebView *web_view) +{ + WebKitWebSettings *settings; + gboolean flattening; + + /* Return TRUE with fail since it's default value we set in _init(). */ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), TRUE); + + settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (web_view)); + g_return_val_if_fail (settings != NULL, TRUE); + + g_object_get ( + G_OBJECT (settings), + "enable-frame-flattening", &flattening, NULL); + + return flattening; +} + +void +e_web_view_set_enable_frame_flattening (EWebView *web_view, + gboolean enable_frame_flattening) +{ + WebKitWebSettings *settings; + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (web_view)); + g_return_if_fail (settings != NULL); + + g_object_set ( + G_OBJECT (settings), "enable-frame-flattening", + enable_frame_flattening, NULL); +} + +gboolean +e_web_view_get_editable (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return webkit_web_view_get_editable (WEBKIT_WEB_VIEW (web_view)); +} + +void +e_web_view_set_editable (EWebView *web_view, + gboolean editable) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + webkit_web_view_set_editable (WEBKIT_WEB_VIEW (web_view), editable); +} + +gboolean +e_web_view_get_inline_spelling (EWebView *web_view) +{ +#if 0 /* WEBKIT - XXX No equivalent property? */ + /* XXX This is just here to maintain symmetry + * with e_web_view_set_inline_spelling(). */ + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return gtk_html_get_inline_spelling (GTK_HTML (web_view)); +#endif + + return FALSE; +} + +void +e_web_view_set_inline_spelling (EWebView *web_view, + gboolean inline_spelling) +{ +#if 0 /* WEBKIT - XXX No equivalent property? */ + /* XXX GtkHTML does not utilize GObject properties as well + * as it could. This just wraps gtk_html_set_inline_spelling() + * so we get a "notify::inline-spelling" signal. */ + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + gtk_html_set_inline_spelling (GTK_HTML (web_view), inline_spelling); + + g_object_notify (G_OBJECT (web_view), "inline-spelling"); +#endif +} + +gboolean +e_web_view_get_magic_links (EWebView *web_view) +{ +#if 0 /* WEBKIT - XXX No equivalent property? */ + /* XXX This is just here to maintain symmetry + * with e_web_view_set_magic_links(). */ + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return gtk_html_get_magic_links (GTK_HTML (web_view)); +#endif + + return FALSE; +} + +void +e_web_view_set_magic_links (EWebView *web_view, + gboolean magic_links) +{ +#if 0 /* WEBKIT - XXX No equivalent property? */ + /* XXX GtkHTML does not utilize GObject properties as well + * as it could. This just wraps gtk_html_set_magic_links() + * so we can get a "notify::magic-links" signal. */ + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + gtk_html_set_magic_links (GTK_HTML (web_view), magic_links); + + g_object_notify (G_OBJECT (web_view), "magic-links"); +#endif +} + +gboolean +e_web_view_get_magic_smileys (EWebView *web_view) +{ +#if 0 /* WEBKIT - No equivalent property? */ + /* XXX This is just here to maintain symmetry + * with e_web_view_set_magic_smileys(). */ + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return gtk_html_get_magic_smileys (GTK_HTML (web_view)); +#endif + + return FALSE; +} + +void +e_web_view_set_magic_smileys (EWebView *web_view, + gboolean magic_smileys) +{ +#if 0 /* WEBKIT - No equivalent property? */ + /* XXX GtkHTML does not utilize GObject properties as well + * as it could. This just wraps gtk_html_set_magic_smileys() + * so we can get a "notify::magic-smileys" signal. */ + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + gtk_html_set_magic_smileys (GTK_HTML (web_view), magic_smileys); + + g_object_notify (G_OBJECT (web_view), "magic-smileys"); +#endif +} + +const gchar * +e_web_view_get_selected_uri (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return web_view->priv->selected_uri; +} + +void +e_web_view_set_selected_uri (EWebView *web_view, + const gchar *selected_uri) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (g_strcmp0 (web_view->priv->selected_uri, selected_uri) == 0) + return; + + g_free (web_view->priv->selected_uri); + web_view->priv->selected_uri = g_strdup (selected_uri); + + g_object_notify (G_OBJECT (web_view), "selected-uri"); +} + +GdkPixbufAnimation * +e_web_view_get_cursor_image (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return web_view->priv->cursor_image; +} + +void +e_web_view_set_cursor_image (EWebView *web_view, + GdkPixbufAnimation *image) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (web_view->priv->cursor_image == image) + return; + + if (image != NULL) + g_object_ref (image); + + if (web_view->priv->cursor_image != NULL) + g_object_unref (web_view->priv->cursor_image); + + web_view->priv->cursor_image = image; + + g_object_notify (G_OBJECT (web_view), "cursor-image"); +} + +const gchar * +e_web_view_get_cursor_image_src (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return web_view->priv->cursor_image_src; +} + +void +e_web_view_set_cursor_image_src (EWebView *web_view, + const gchar *src_uri) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (g_strcmp0 (web_view->priv->cursor_image_src, src_uri) == 0) + return; + + g_free (web_view->priv->cursor_image_src); + web_view->priv->cursor_image_src = g_strdup (src_uri); + + g_object_notify (G_OBJECT (web_view), "cursor-image-src"); +} + +GtkAction * +e_web_view_get_open_proxy (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return web_view->priv->open_proxy; +} + +void +e_web_view_set_open_proxy (EWebView *web_view, + GtkAction *open_proxy) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (web_view->priv->open_proxy == open_proxy) + return; + + if (open_proxy != NULL) { + g_return_if_fail (GTK_IS_ACTION (open_proxy)); + g_object_ref (open_proxy); + } + + if (web_view->priv->open_proxy != NULL) + g_object_unref (web_view->priv->open_proxy); + + web_view->priv->open_proxy = open_proxy; + + g_object_notify (G_OBJECT (web_view), "open-proxy"); +} + +GtkTargetList * +e_web_view_get_paste_target_list (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return webkit_web_view_get_paste_target_list ( + WEBKIT_WEB_VIEW (web_view)); +} + +GtkAction * +e_web_view_get_print_proxy (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return web_view->priv->print_proxy; +} + +void +e_web_view_set_print_proxy (EWebView *web_view, + GtkAction *print_proxy) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (web_view->priv->print_proxy == print_proxy) + return; + + if (print_proxy != NULL) { + g_return_if_fail (GTK_IS_ACTION (print_proxy)); + g_object_ref (print_proxy); + } + + if (web_view->priv->print_proxy != NULL) + g_object_unref (web_view->priv->print_proxy); + + web_view->priv->print_proxy = print_proxy; + + g_object_notify (G_OBJECT (web_view), "print-proxy"); +} + +GtkAction * +e_web_view_get_save_as_proxy (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return web_view->priv->save_as_proxy; +} + +void +e_web_view_set_save_as_proxy (EWebView *web_view, + GtkAction *save_as_proxy) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (web_view->priv->save_as_proxy == save_as_proxy) + return; + + if (save_as_proxy != NULL) { + g_return_if_fail (GTK_IS_ACTION (save_as_proxy)); + g_object_ref (save_as_proxy); + } + + if (web_view->priv->save_as_proxy != NULL) + g_object_unref (web_view->priv->save_as_proxy); + + web_view->priv->save_as_proxy = save_as_proxy; + + g_object_notify (G_OBJECT (web_view), "save-as-proxy"); +} + +GSList * +e_web_view_get_highlights (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return web_view->priv->highlights; +} + +void +e_web_view_add_highlight (EWebView *web_view, + const gchar *highlight) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + g_return_if_fail (highlight && *highlight); + + web_view->priv->highlights = g_slist_append ( + web_view->priv->highlights, g_strdup (highlight)); + + web_view_update_document_highlights (web_view); +} + +void e_web_view_clear_highlights (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (!web_view->priv->highlights) + return; + + g_slist_free_full (web_view->priv->highlights, g_free); + web_view->priv->highlights = NULL; + + web_view_update_document_highlights (web_view); +} + +GtkAction * +e_web_view_get_action (EWebView *web_view, + const gchar *action_name) +{ + GtkUIManager *ui_manager; + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + g_return_val_if_fail (action_name != NULL, NULL); + + ui_manager = e_web_view_get_ui_manager (web_view); + + return e_lookup_action (ui_manager, action_name); +} + +GtkActionGroup * +e_web_view_get_action_group (EWebView *web_view, + const gchar *group_name) +{ + GtkUIManager *ui_manager; + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + g_return_val_if_fail (group_name != NULL, NULL); + + ui_manager = e_web_view_get_ui_manager (web_view); + + return e_lookup_action_group (ui_manager, group_name); +} + +gchar * +e_web_view_extract_uri (EWebView *web_view, + GdkEventButton *event) +{ + EWebViewClass *class; + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + class = E_WEB_VIEW_GET_CLASS (web_view); + g_return_val_if_fail (class->extract_uri != NULL, NULL); + + return class->extract_uri (web_view, event); +} + +void +e_web_view_copy_clipboard (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (web_view)); +} + +void +e_web_view_cut_clipboard (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + webkit_web_view_cut_clipboard (WEBKIT_WEB_VIEW (web_view)); +} + +gboolean +e_web_view_is_selection_active (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (web_view)); +} + +void +e_web_view_paste_clipboard (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + webkit_web_view_paste_clipboard (WEBKIT_WEB_VIEW (web_view)); +} + +gboolean +e_web_view_scroll_forward (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + webkit_web_view_move_cursor ( + WEBKIT_WEB_VIEW (web_view), GTK_MOVEMENT_PAGES, 1); + + return TRUE; /* XXX This means nothing. */ +} + +gboolean +e_web_view_scroll_backward (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), FALSE); + + webkit_web_view_move_cursor ( + WEBKIT_WEB_VIEW (web_view), GTK_MOVEMENT_PAGES, -1); + + return TRUE; /* XXX This means nothing. */ +} + +void +e_web_view_select_all (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + webkit_web_view_select_all (WEBKIT_WEB_VIEW (web_view)); +} + +void +e_web_view_unselect_all (EWebView *web_view) +{ +#if 0 /* WEBKIT */ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + gtk_html_command (GTK_HTML (web_view), "unselect-all"); +#endif +} + +void +e_web_view_zoom_100 (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + webkit_web_view_set_zoom_level (WEBKIT_WEB_VIEW (web_view), 1.0); +} + +void +e_web_view_zoom_in (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + webkit_web_view_zoom_in (WEBKIT_WEB_VIEW (web_view)); +} + +void +e_web_view_zoom_out (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + webkit_web_view_zoom_out (WEBKIT_WEB_VIEW (web_view)); +} + +GtkUIManager * +e_web_view_get_ui_manager (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return web_view->priv->ui_manager; +} + +GtkWidget * +e_web_view_get_popup_menu (EWebView *web_view) +{ + GtkUIManager *ui_manager; + GtkWidget *menu; + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + ui_manager = e_web_view_get_ui_manager (web_view); + menu = gtk_ui_manager_get_widget (ui_manager, "/context"); + g_return_val_if_fail (GTK_IS_MENU (menu), NULL); + + return menu; +} + +void +e_web_view_show_popup_menu (EWebView *web_view) +{ + GtkWidget *menu; + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + e_web_view_update_actions (web_view); + + menu = e_web_view_get_popup_menu (web_view); + + gtk_menu_popup ( + GTK_MENU (menu), NULL, NULL, NULL, NULL, + 0, gtk_get_current_event_time ()); +} + +void +e_web_view_status_message (EWebView *web_view, + const gchar *status_message) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + g_signal_emit (web_view, signals[STATUS_MESSAGE], 0, status_message); +} + +void +e_web_view_stop_loading (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + g_signal_emit (web_view, signals[STOP_LOADING], 0); +} + +void +e_web_view_update_actions (EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + g_signal_emit (web_view, signals[UPDATE_ACTIONS], 0); +} + +static gchar * +web_view_get_frame_selection_html (WebKitDOMElement *iframe) +{ + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *selection; + WebKitDOMNodeList *frames; + gulong ii, length; + + document = webkit_dom_html_iframe_element_get_content_document ( + WEBKIT_DOM_HTML_IFRAME_ELEMENT (iframe)); + window = webkit_dom_document_get_default_view (document); + selection = webkit_dom_dom_window_get_selection (window); + if (selection && (webkit_dom_dom_selection_get_range_count (selection) > 0)) { + WebKitDOMRange *range; + WebKitDOMElement *element; + WebKitDOMDocumentFragment *fragment; + + range = webkit_dom_dom_selection_get_range_at (selection, 0, NULL); + if (range != NULL) { + fragment = webkit_dom_range_clone_contents ( + range, NULL); + + element = webkit_dom_document_create_element ( + document, "DIV", NULL); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), + WEBKIT_DOM_NODE (fragment), NULL); + + return webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); + } + } + + frames = webkit_dom_document_get_elements_by_tag_name ( + document, "IFRAME"); + length = webkit_dom_node_list_get_length (frames); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node; + gchar *text; + + node = webkit_dom_node_list_item (frames, ii); + + text = web_view_get_frame_selection_html ( + WEBKIT_DOM_ELEMENT (node)); + + if (text != NULL) + return text; + } + + return NULL; +} + +gchar * +e_web_view_get_selection_html (EWebView *web_view) +{ + WebKitDOMDocument *document; + WebKitDOMNodeList *frames; + gulong ii, length; + + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + if (!webkit_web_view_has_selection (WEBKIT_WEB_VIEW (web_view))) + return NULL; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (web_view)); + frames = webkit_dom_document_get_elements_by_tag_name (document, "IFRAME"); + length = webkit_dom_node_list_get_length (frames); + + for (ii = 0; ii < length; ii++) { + gchar *text; + WebKitDOMNode *node; + + node = webkit_dom_node_list_item (frames, ii); + + text = web_view_get_frame_selection_html ( + WEBKIT_DOM_ELEMENT (node)); + + if (text != NULL) + return text; + } + + return NULL; +} + +void +e_web_view_set_settings (EWebView *web_view, + WebKitWebSettings *settings) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + if (settings == webkit_web_view_get_settings (WEBKIT_WEB_VIEW (web_view))) + return; + + g_object_bind_property ( + settings, "enable-caret-browsing", + web_view, "caret-mode", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + webkit_web_view_set_settings (WEBKIT_WEB_VIEW (web_view), settings); +} + +WebKitWebSettings * +e_web_view_get_default_settings (void) +{ + WebKitWebSettings *settings; + + settings = webkit_web_settings_new (); + + g_object_set ( + G_OBJECT (settings), + "enable-frame-flattening", TRUE, + "enable-java-applet", FALSE, + "enable-html5-database", FALSE, + "enable-html5-local-storage", FALSE, + "enable-offline-web-application-cache", FALSE, + "enable-site-specific-quirks", TRUE, + "enable-scripts", FALSE, + NULL); + + return settings; +} + +void +e_web_view_update_fonts (EWebView *web_view) +{ + EWebViewClass *class; + GString *stylesheet; + gchar *base64; + gchar *aa = NULL; + WebKitWebSettings *settings; + PangoFontDescription *min_size, *ms, *vw; + const gchar *styles[] = { "normal", "oblique", "italic" }; + const gchar *smoothing = NULL; + GtkStyleContext *context; + GdkColor *link = NULL; + GdkColor *visited = NULL; + + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + + ms = NULL; + vw = NULL; + + class = E_WEB_VIEW_GET_CLASS (web_view); + if (class->set_fonts != NULL) + class->set_fonts (web_view, &ms, &vw); + + if (ms == NULL) { + gchar *font; + + font = g_settings_get_string ( + web_view->priv->font_settings, + "monospace-font-name"); + + ms = pango_font_description_from_string ( + (font != NULL) ? font : "monospace 10"); + + g_free (font); + } + + if (vw == NULL) { + gchar *font; + + font = g_settings_get_string ( + web_view->priv->font_settings, + "font-name"); + + vw = pango_font_description_from_string ( + (font != NULL) ? font : "serif 10"); + + g_free (font); + } + + if (pango_font_description_get_size (ms) < pango_font_description_get_size (vw)) { + min_size = ms; + } else { + min_size = vw; + } + + stylesheet = g_string_new (""); + g_string_append_printf ( + stylesheet, + "body {\n" + " font-family: '%s';\n" + " font-size: %dpt;\n" + " font-weight: %d;\n" + " font-style: %s;\n", + pango_font_description_get_family (vw), + pango_font_description_get_size (vw) / PANGO_SCALE, + pango_font_description_get_weight (vw), + styles[pango_font_description_get_style (vw)]); + + if (web_view->priv->aliasing_settings != NULL) + aa = g_settings_get_string ( + web_view->priv->aliasing_settings, "antialiasing"); + + if (g_strcmp0 (aa, "none") == 0) + smoothing = "none"; + else if (g_strcmp0 (aa, "grayscale") == 0) + smoothing = "antialiased"; + else if (g_strcmp0 (aa, "rgba") == 0) + smoothing = "subpixel-antialiased"; + + if (smoothing != NULL) + g_string_append_printf ( + stylesheet, + " -webkit-font-smoothing: %s;\n", + smoothing); + + g_free (aa); + + g_string_append (stylesheet, "}\n"); + + g_string_append_printf ( + stylesheet, + "pre,code,.pre {\n" + " font-family: '%s';\n" + " font-size: %dpt;\n" + " font-weight: %d;\n" + " font-style: %s;\n" + "}", + pango_font_description_get_family (ms), + pango_font_description_get_size (ms) / PANGO_SCALE, + pango_font_description_get_weight (ms), + styles[pango_font_description_get_style (ms)]); + + context = gtk_widget_get_style_context (GTK_WIDGET (web_view)); + gtk_style_context_get_style ( + context, + "link-color", &link, + "visited-link-color", &visited, + NULL); + + if (link == NULL) { + link = g_slice_new0 (GdkColor); + link->blue = G_MAXINT16; + } + + if (visited == NULL) { + visited = g_slice_new0 (GdkColor); + visited->red = G_MAXINT16; + } + + g_string_append_printf ( + stylesheet, + "a {\n" + " color: #%06x;\n" + "}\n" + "a:visited {\n" + " color: #%06x;\n" + "}\n", + e_color_to_value (link), + e_color_to_value (visited)); + + gdk_color_free (link); + gdk_color_free (visited); + + base64 = g_base64_encode ((guchar *) stylesheet->str, stylesheet->len); + g_string_free (stylesheet, TRUE); + + stylesheet = g_string_new ("data:text/css;charset=utf-8;base64,"); + g_string_append (stylesheet, base64); + g_free (base64); + + settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (web_view)); + g_object_set ( + G_OBJECT (settings), + "default-font-size", pango_font_description_get_size (vw) / PANGO_SCALE, + "default-font-family", pango_font_description_get_family (vw), + "monospace-font-family", pango_font_description_get_family (ms), + "default-monospace-font-size", (pango_font_description_get_size (ms) / PANGO_SCALE), + "minimum-font-size", (pango_font_description_get_size (min_size) / PANGO_SCALE), + "user-stylesheet-uri", stylesheet->str, + NULL); + + g_string_free (stylesheet, TRUE); + + pango_font_description_free (ms); + pango_font_description_free (vw); +} + +void +e_web_view_install_request_handler (EWebView *web_view, + GType handler_type) +{ + SoupSession *session; + SoupSessionFeature *feature; + gboolean new; + + session = webkit_get_default_session (); + + feature = soup_session_get_feature (session, SOUP_TYPE_REQUESTER); + new = FALSE; + if (feature == NULL) { + feature = SOUP_SESSION_FEATURE (soup_requester_new ()); + soup_session_add_feature (session, feature); + new = TRUE; + } + + soup_session_feature_add_feature (feature, handler_type); + + if (new) { + g_object_unref (feature); + } +} + diff --git a/e-util/e-web-view.h b/e-util/e-web-view.h new file mode 100644 index 0000000000..6690725b86 --- /dev/null +++ b/e-util/e-web-view.h @@ -0,0 +1,224 @@ +/* + * e-web-view.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +/* This is intended to serve as a common base class for all HTML viewing + * needs in Evolution. Currently based on GtkHTML, the idea is to wrap + * the GtkHTML API enough that we no longer have to make direct calls to + * it. This should help smooth the transition to WebKit/GTK+. + * + * This class handles basic tasks like mouse hovers over links, clicked + * links, and servicing URI requests asynchronously via GIO. */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_WEB_VIEW_H +#define E_WEB_VIEW_H + +#include <webkit/webkit.h> + +/* Standard GObject macros */ +#define E_TYPE_WEB_VIEW \ + (e_web_view_get_type ()) +#define E_WEB_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_WEB_VIEW, EWebView)) +#define E_WEB_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_WEB_VIEW, EWebViewClass)) +#define E_IS_WEB_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_WEB_VIEW)) +#define E_IS_WEB_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_WEB_VIEW)) +#define E_WEB_VIEW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_WEB_VIEW, EWebViewClass)) + +G_BEGIN_DECLS + +typedef struct _EWebView EWebView; +typedef struct _EWebViewClass EWebViewClass; +typedef struct _EWebViewPrivate EWebViewPrivate; +struct PangoFontDescription; + +struct _EWebView { + WebKitWebView parent; + EWebViewPrivate *priv; +}; + +typedef void (*EWebViewJSFunctionCallback) (EWebView *web_view, + size_t arg_count, + const JSValueRef args[], + gpointer user_data); + +struct _EWebViewClass { + WebKitWebViewClass parent_class; + + /* Methods */ + GtkWidget * (*create_plugin_widget) (EWebView *web_view, + const gchar *mime_type, + const gchar *uri, + GHashTable *param); + gchar * (*extract_uri) (EWebView *web_view, + GdkEventButton *event); + void (*hovering_over_link) (EWebView *web_view, + const gchar *title, + const gchar *uri); + void (*link_clicked) (EWebView *web_view, + const gchar *uri); + void (*load_string) (EWebView *web_view, + const gchar *load_string); + void (*load_uri) (EWebView *web_view, + const gchar *load_uri); + void (*frame_load_string) (EWebView *web_view, + const gchar *frame_name, + const gchar *string); + void (*frame_load_uri) (EWebView *web_view, + const gchar *frame_name, + const gchar *uri); + void (*set_fonts) (EWebView *web_view, + PangoFontDescription **monospace, + PangoFontDescription **variable_width); + + /* Signals */ + gboolean (*popup_event) (EWebView *web_view, + const gchar *uri); + void (*status_message) (EWebView *web_view, + const gchar *status_message); + void (*stop_loading) (EWebView *web_view); + void (*update_actions) (EWebView *web_view); + gboolean (*process_mailto) (EWebView *web_view, + const gchar *mailto_uri); +}; + +GType e_web_view_get_type (void); +GtkWidget * e_web_view_new (void); +void e_web_view_clear (EWebView *web_view); +void e_web_view_load_string (EWebView *web_view, + const gchar *string); +void e_web_view_load_uri (EWebView *web_view, + const gchar *uri); +const gchar * e_web_view_get_uri (EWebView *web_view); +void e_web_view_reload (EWebView *web_view); +void e_web_view_frame_load_string (EWebView *web_view, + const gchar *frame_name, + const gchar *string); +void e_web_view_frame_load_uri (EWebView *web_view, + const gchar *frame_name, + const gchar *uri); +const gchar * e_web_view_frame_get_uri (EWebView *web_view, + const gchar *frame_name); +gchar * e_web_view_get_html (EWebView *web_view); +gboolean e_web_view_get_caret_mode (EWebView *web_view); +void e_web_view_set_caret_mode (EWebView *web_view, + gboolean caret_mode); +GtkTargetList * e_web_view_get_copy_target_list (EWebView *web_view); +gboolean e_web_view_get_disable_printing (EWebView *web_view); +void e_web_view_set_disable_printing (EWebView *web_view, + gboolean disable_printing); +gboolean e_web_view_get_disable_save_to_disk + (EWebView *web_view); +void e_web_view_set_disable_save_to_disk + (EWebView *web_view, + gboolean disable_save_to_disk); +gboolean e_web_view_get_enable_frame_flattening + (EWebView *web_view); +void e_web_view_set_enable_frame_flattening + (EWebView *web_view, + gboolean enable_frame_flattening); +gboolean e_web_view_get_editable (EWebView *web_view); +void e_web_view_set_editable (EWebView *web_view, + gboolean editable); +gboolean e_web_view_get_inline_spelling (EWebView *web_view); +void e_web_view_set_inline_spelling (EWebView *web_view, + gboolean inline_spelling); +gboolean e_web_view_get_magic_links (EWebView *web_view); +void e_web_view_set_magic_links (EWebView *web_view, + gboolean magic_links); +gboolean e_web_view_get_magic_smileys (EWebView *web_view); +void e_web_view_set_magic_smileys (EWebView *web_view, + gboolean magic_smileys); +const gchar * e_web_view_get_selected_uri (EWebView *web_view); +void e_web_view_set_selected_uri (EWebView *web_view, + const gchar *selected_uri); +GdkPixbufAnimation * + e_web_view_get_cursor_image (EWebView *web_view); +void e_web_view_set_cursor_image (EWebView *web_view, + GdkPixbufAnimation *animation); +const gchar * e_web_view_get_cursor_image_src (EWebView *web_view); +void e_web_view_set_cursor_image_src (EWebView *web_view, + const gchar *src_uri); +GtkAction * e_web_view_get_open_proxy (EWebView *web_view); +void e_web_view_set_open_proxy (EWebView *web_view, + GtkAction *open_proxy); +GtkTargetList * e_web_view_get_paste_target_list + (EWebView *web_view); +GtkAction * e_web_view_get_print_proxy (EWebView *web_view); +void e_web_view_set_print_proxy (EWebView *web_view, + GtkAction *print_proxy); +GtkAction * e_web_view_get_save_as_proxy (EWebView *web_view); +void e_web_view_set_save_as_proxy (EWebView *web_view, + GtkAction *save_as_proxy); +GSList * e_web_view_get_highlights (EWebView *web_view); +void e_web_view_add_highlight (EWebView *web_view, + const gchar *highlight); +void e_web_view_clear_highlights (EWebView *web_view); +GtkAction * e_web_view_get_action (EWebView *web_view, + const gchar *action_name); +GtkActionGroup *e_web_view_get_action_group (EWebView *web_view, + const gchar *group_name); +gchar * e_web_view_extract_uri (EWebView *web_view, + GdkEventButton *event); +void e_web_view_copy_clipboard (EWebView *web_view); +void e_web_view_cut_clipboard (EWebView *web_view); +gboolean e_web_view_is_selection_active (EWebView *web_view); +void e_web_view_paste_clipboard (EWebView *web_view); +gboolean e_web_view_scroll_forward (EWebView *web_view); +gboolean e_web_view_scroll_backward (EWebView *web_view); +void e_web_view_select_all (EWebView *web_view); +void e_web_view_unselect_all (EWebView *web_view); +void e_web_view_zoom_100 (EWebView *web_view); +void e_web_view_zoom_in (EWebView *web_view); +void e_web_view_zoom_out (EWebView *web_view); +GtkUIManager * e_web_view_get_ui_manager (EWebView *web_view); +GtkWidget * e_web_view_get_popup_menu (EWebView *web_view); +void e_web_view_show_popup_menu (EWebView *web_view); +void e_web_view_status_message (EWebView *web_view, + const gchar *status_message); +void e_web_view_stop_loading (EWebView *web_view); +void e_web_view_update_actions (EWebView *web_view); +gchar * e_web_view_get_selection_html (EWebView *web_view); + +void e_web_view_set_settings (EWebView *web_view, + WebKitWebSettings *settings); + +void e_web_view_update_fonts (EWebView *web_view); + +WebKitWebSettings * + e_web_view_get_default_settings (void); + +void e_web_view_install_request_handler + (EWebView *web_view, + GType handler_type); + +G_END_DECLS + +#endif /* E_WEB_VIEW_H */ diff --git a/e-util/e-xml-utils.c b/e-util/e-xml-utils.c new file mode 100644 index 0000000000..aaa66b6010 --- /dev/null +++ b/e-util/e-xml-utils.c @@ -0,0 +1,448 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-xml-utils.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <locale.h> +#include <unistd.h> +#include <fcntl.h> +#include <errno.h> +#include <math.h> +#include <string.h> + +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include "e-misc-utils.h" + +/* Returns the first child with the name child_name and the "lang" + * attribute that matches the current LC_MESSAGES, or else, the first + * child with the name child_name and no "lang" attribute. + */ +xmlNode * +e_xml_get_child_by_name_by_lang (const xmlNode *parent, + const xmlChar *child_name, + const gchar *lang) +{ +#ifdef G_OS_WIN32 + gchar *freeme = NULL; +#endif + xmlNode *child; + /* This is the default version of the string. */ + xmlNode *C = NULL; + + g_return_val_if_fail (parent != NULL, NULL); + g_return_val_if_fail (child_name != NULL, NULL); + + if (lang == NULL) { +#ifndef G_OS_WIN32 +#ifdef HAVE_LC_MESSAGES + lang = setlocale (LC_MESSAGES, NULL); +#else + lang = setlocale (LC_CTYPE, NULL); +#endif +#else + lang = freeme = g_win32_getlocale (); +#endif + } + for (child = parent->xmlChildrenNode; child != NULL; child = child->next) { + if (xmlStrcmp (child->name, child_name) == 0) { + xmlChar *this_lang = xmlGetProp ( + child, (const guchar *)"lang"); + if (this_lang == NULL) { + C = child; + } else if (xmlStrcmp (this_lang, (xmlChar *) lang) == 0) { +#ifdef G_OS_WIN32 + g_free (freeme); +#endif + return child; + } + } + } +#ifdef G_OS_WIN32 + g_free (freeme); +#endif + return C; +} + +static xmlNode * +e_xml_get_child_by_name_by_lang_list_with_score (const xmlNode *parent, + const gchar *name, + const GList *lang_list, + gint *best_lang_score) +{ + xmlNodePtr best_node = NULL, node; + + for (node = parent->xmlChildrenNode; node != NULL; node = node->next) { + xmlChar *lang; + + if (node->name == NULL || strcmp ((gchar *) node->name, name) != 0) { + continue; + } + lang = xmlGetProp (node, (const guchar *)"xml:lang"); + if (lang != NULL) { + const GList *l; + gint i; + + for (l = lang_list, i = 0; + l != NULL && i < *best_lang_score; + l = l->next, i++) { + if (strcmp ((gchar *) l->data, (gchar *) lang) == 0) { + best_node = node; + *best_lang_score = i; + } + } + } else { + if (best_node == NULL) { + best_node = node; + } + } + xmlFree (lang); + if (*best_lang_score == 0) { + return best_node; + } + } + + return best_node; +} + +xmlNode * +e_xml_get_child_by_name_by_lang_list (const xmlNode *parent, + const gchar *name, + const GList *lang_list) +{ + gint best_lang_score = INT_MAX; + + g_return_val_if_fail (parent != NULL, NULL); + g_return_val_if_fail (name != NULL, NULL); + + if (lang_list == NULL) { + const gchar * const *language_names; + + language_names = g_get_language_names (); + while (*language_names != NULL) + lang_list = g_list_append ( + (GList *) lang_list, (gchar *) * language_names++); + } + return e_xml_get_child_by_name_by_lang_list_with_score + (parent,name, + lang_list, + &best_lang_score); +} + +xmlNode * +e_xml_get_child_by_name_no_lang (const xmlNode *parent, + const gchar *name) +{ + xmlNodePtr node; + + g_return_val_if_fail (parent != NULL, NULL); + g_return_val_if_fail (name != NULL, NULL); + + for (node = parent->xmlChildrenNode; node != NULL; node = node->next) { + xmlChar *lang; + + if (node->name == NULL || strcmp ((gchar *) node->name, name) != 0) { + continue; + } + lang = xmlGetProp (node, (const guchar *)"xml:lang"); + if (lang == NULL) { + return node; + } + xmlFree (lang); + } + + return NULL; +} + +gint +e_xml_get_integer_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name) +{ + g_return_val_if_fail (parent != NULL, 0); + g_return_val_if_fail (prop_name != NULL, 0); + + return e_xml_get_integer_prop_by_name_with_default (parent, prop_name, 0); +} + +gint +e_xml_get_integer_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + gint def) +{ + xmlChar *prop; + gint ret_val = def; + + g_return_val_if_fail (parent != NULL, 0); + g_return_val_if_fail (prop_name != NULL, 0); + + prop = xmlGetProp ((xmlNode *) parent, prop_name); + if (prop != NULL) { + (void) sscanf ((gchar *) prop, "%d", &ret_val); + xmlFree (prop); + } + return ret_val; +} + +void +e_xml_set_integer_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + gint value) +{ + gchar *valuestr; + + g_return_if_fail (parent != NULL); + g_return_if_fail (prop_name != NULL); + + valuestr = g_strdup_printf ("%d", value); + xmlSetProp (parent, prop_name, (guchar *) valuestr); + g_free (valuestr); +} + +guint +e_xml_get_uint_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name) +{ + g_return_val_if_fail (parent != NULL, 0); + g_return_val_if_fail (prop_name != NULL, 0); + + return e_xml_get_uint_prop_by_name_with_default (parent, prop_name, 0); +} + +guint +e_xml_get_uint_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + guint def) +{ + xmlChar *prop; + guint ret_val = def; + + g_return_val_if_fail (parent != NULL, 0); + g_return_val_if_fail (prop_name != NULL, 0); + + prop = xmlGetProp ((xmlNode *) parent, prop_name); + if (prop != NULL) { + (void) sscanf ((gchar *) prop, "%u", &ret_val); + xmlFree (prop); + } + return ret_val; +} + +void +e_xml_set_uint_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + guint value) +{ + gchar *valuestr; + + g_return_if_fail (parent != NULL); + g_return_if_fail (prop_name != NULL); + + valuestr = g_strdup_printf ("%u", value); + xmlSetProp (parent, prop_name, (guchar *) valuestr); + g_free (valuestr); +} + +gboolean +e_xml_get_bool_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name) +{ + g_return_val_if_fail (parent != NULL, 0); + g_return_val_if_fail (prop_name != NULL, 0); + + return e_xml_get_bool_prop_by_name_with_default ( + parent, prop_name, FALSE); +} + +gboolean +e_xml_get_bool_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + gboolean def) +{ + xmlChar *prop; + gboolean ret_val = def; + + g_return_val_if_fail (parent != NULL, 0); + g_return_val_if_fail (prop_name != NULL, 0); + + prop = xmlGetProp ((xmlNode *) parent, prop_name); + if (prop != NULL) { + if (g_ascii_strcasecmp ((gchar *) prop, "true") == 0) { + ret_val = TRUE; + } else if (g_ascii_strcasecmp ((gchar *) prop, "false") == 0) { + ret_val = FALSE; + } + xmlFree (prop); + } + return ret_val; +} + +void +e_xml_set_bool_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + gboolean value) +{ + g_return_if_fail (parent != NULL); + g_return_if_fail (prop_name != NULL); + + if (value) { + xmlSetProp (parent, prop_name, (const guchar *)"true"); + } else { + xmlSetProp (parent, prop_name, (const guchar *)"false"); + } +} + +gdouble +e_xml_get_double_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name) +{ + g_return_val_if_fail (parent != NULL, 0); + g_return_val_if_fail (prop_name != NULL, 0); + + return e_xml_get_double_prop_by_name_with_default (parent, prop_name, 0.0); +} + +gdouble +e_xml_get_double_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + gdouble def) +{ + xmlChar *prop; + gdouble ret_val = def; + + g_return_val_if_fail (parent != NULL, 0); + g_return_val_if_fail (prop_name != NULL, 0); + + prop = xmlGetProp ((xmlNode *) parent, prop_name); + if (prop != NULL) { + ret_val = e_flexible_strtod ((gchar *) prop, NULL); + xmlFree (prop); + } + return ret_val; +} + +void +e_xml_set_double_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + gdouble value) +{ + gchar buffer[E_ASCII_DTOSTR_BUF_SIZE]; + gchar *format; + + g_return_if_fail (parent != NULL); + g_return_if_fail (prop_name != NULL); + + if (fabs (value) < 1e9 && fabs (value) > 1e-5) { + format = g_strdup_printf ("%%.%df", DBL_DIG); + } else { + format = g_strdup_printf ("%%.%dg", DBL_DIG); + } + e_ascii_dtostr (buffer, sizeof (buffer), format, value); + g_free (format); + + xmlSetProp (parent, prop_name, (const guchar *) buffer); +} + +gchar * +e_xml_get_string_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name) +{ + g_return_val_if_fail (parent != NULL, NULL); + g_return_val_if_fail (prop_name != NULL, NULL); + + return e_xml_get_string_prop_by_name_with_default (parent, prop_name, NULL); +} + +gchar * +e_xml_get_string_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + const gchar *def) +{ + xmlChar *prop; + gchar *ret_val; + + g_return_val_if_fail (parent != NULL, NULL); + g_return_val_if_fail (prop_name != NULL, NULL); + + prop = xmlGetProp ((xmlNode *) parent, prop_name); + if (prop != NULL) { + ret_val = g_strdup ((gchar *) prop); + xmlFree (prop); + } else { + ret_val = g_strdup (def); + } + return ret_val; +} + +void +e_xml_set_string_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + const gchar *value) +{ + g_return_if_fail (parent != NULL); + g_return_if_fail (prop_name != NULL); + + if (value != NULL) { + xmlSetProp (parent, prop_name, (guchar *) value); + } +} + +gchar * +e_xml_get_translated_string_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name) +{ + xmlChar *prop; + gchar *ret_val = NULL; + gchar *combined_name; + + g_return_val_if_fail (parent != NULL, NULL); + g_return_val_if_fail (prop_name != NULL, NULL); + + prop = xmlGetProp ((xmlNode *) parent, prop_name); + if (prop != NULL) { + ret_val = g_strdup ((gchar *) prop); + xmlFree (prop); + return ret_val; + } + + combined_name = g_strdup_printf ("_%s", prop_name); + prop = xmlGetProp ((xmlNode *) parent, (guchar *) combined_name); + if (prop != NULL) { + ret_val = g_strdup (gettext ((gchar *) prop)); + xmlFree (prop); + } + g_free (combined_name); + + return ret_val; +} + diff --git a/e-util/e-xml-utils.h b/e-util/e-xml-utils.h new file mode 100644 index 0000000000..9796569774 --- /dev/null +++ b/e-util/e-xml-utils.h @@ -0,0 +1,93 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifndef __E_XML_UTILS__ +#define __E_XML_UTILS__ + +#include <glib.h> + +#include <libxml/tree.h> + +G_BEGIN_DECLS + +/* lang set to NULL means use the current locale. */ +xmlNode *e_xml_get_child_by_name_by_lang (const xmlNode *parent, + const xmlChar *child_name, + const gchar *lang); +/* lang_list set to NULL means use the current locale. */ +xmlNode *e_xml_get_child_by_name_by_lang_list (const xmlNode *parent, + const gchar *name, + const GList *lang_list); +xmlNode *e_xml_get_child_by_name_no_lang (const xmlNode *parent, + const gchar *name); + +gint e_xml_get_integer_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name); +gint e_xml_get_integer_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + gint def); +void e_xml_set_integer_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + gint value); + +guint e_xml_get_uint_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name); +guint e_xml_get_uint_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + guint def); +void e_xml_set_uint_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + guint value); + +gboolean e_xml_get_bool_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name); +gboolean e_xml_get_bool_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + gboolean def); +void e_xml_set_bool_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + gboolean value); + +gdouble e_xml_get_double_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name); +gdouble e_xml_get_double_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + gdouble def); +void e_xml_set_double_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + gdouble value); + +gchar *e_xml_get_string_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name); +gchar *e_xml_get_string_prop_by_name_with_default (const xmlNode *parent, + const xmlChar *prop_name, + const gchar *def); +void e_xml_set_string_prop_by_name (xmlNode *parent, + const xmlChar *prop_name, + const gchar *value); + +gchar *e_xml_get_translated_string_prop_by_name (const xmlNode *parent, + const xmlChar *prop_name); + +G_END_DECLS + +#endif /* __E_XML_UTILS__ */ diff --git a/e-util/ea-calendar-cell.c b/e-util/ea-calendar-cell.c new file mode 100644 index 0000000000..a9784df31a --- /dev/null +++ b/e-util/ea-calendar-cell.c @@ -0,0 +1,404 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> +#include "ea-calendar-cell.h" +#include "ea-calendar-item.h" +#include "ea-factory.h" + +/* ECalendarCell */ + +static void e_calendar_cell_class_init (ECalendarCellClass *class); + +EA_FACTORY_GOBJECT (EA_TYPE_CALENDAR_CELL, ea_calendar_cell, ea_calendar_cell_new) + +GType +e_calendar_cell_get_type (void) +{ + static GType type = 0; + + if (!type) { + static GTypeInfo tinfo = { + sizeof (ECalendarCellClass), + (GBaseInitFunc) NULL, /* base init */ + (GBaseFinalizeFunc) NULL, /* base finalize */ + (GClassInitFunc) e_calendar_cell_class_init, /* class init */ + (GClassFinalizeFunc) NULL, /* class finalize */ + NULL, /* class data */ + sizeof (ECalendarCell), /* instance size */ + 0, /* nb preallocs */ + (GInstanceInitFunc) NULL, /* instance init */ + NULL /* value table */ + }; + + type = g_type_register_static ( + G_TYPE_OBJECT, + "ECalendarCell", &tinfo, 0); + } + + return type; +} + +static void +e_calendar_cell_class_init (ECalendarCellClass *class) +{ + EA_SET_FACTORY (e_calendar_cell_get_type (), ea_calendar_cell); +} + +ECalendarCell * +e_calendar_cell_new (ECalendarItem *calitem, + gint row, + gint column) +{ + GObject *object; + ECalendarCell *cell; + + g_return_val_if_fail (E_IS_CALENDAR_ITEM (calitem), NULL); + + object = g_object_new (E_TYPE_CALENDAR_CELL, NULL); + cell = E_CALENDAR_CELL (object); + cell->calitem = calitem; + cell->row = row; + cell->column = column; + +#ifdef ACC_DEBUG + g_print ("EvoAcc: e_calendar_cell created %p\n", (gpointer) cell); +#endif + + return cell; +} + +/* EaCalendarCell */ + +static void ea_calendar_cell_class_init (EaCalendarCellClass *klass); +static void ea_calendar_cell_init (EaCalendarCell *a11y); + +static const gchar * ea_calendar_cell_get_name (AtkObject *accessible); +static const gchar * ea_calendar_cell_get_description (AtkObject *accessible); +static AtkObject * ea_calendar_cell_get_parent (AtkObject *accessible); +static gint ea_calendar_cell_get_index_in_parent (AtkObject *accessible); +static AtkStateSet *ea_calendar_cell_ref_state_set (AtkObject *accessible); + +/* component interface */ +static void atk_component_interface_init (AtkComponentIface *iface); +static void component_interface_get_extents (AtkComponent *component, + gint *x, gint *y, + gint *width, gint *height, + AtkCoordType coord_type); +static gboolean component_interface_grab_focus (AtkComponent *component); + +static gpointer parent_class = NULL; + +#ifdef ACC_DEBUG +static gint n_ea_calendar_cell_created = 0, n_ea_calendar_cell_destroyed = 0; +static void ea_calendar_cell_finalize (GObject *object); +#endif + +GType +ea_calendar_cell_get_type (void) +{ + static GType type = 0; + + if (!type) { + static GTypeInfo tinfo = { + sizeof (EaCalendarCellClass), + (GBaseInitFunc) NULL, /* base init */ + (GBaseFinalizeFunc) NULL, /* base finalize */ + (GClassInitFunc) ea_calendar_cell_class_init, /* class init */ + (GClassFinalizeFunc) NULL, /* class finalize */ + NULL, /* class data */ + sizeof (EaCalendarCell), /* instance size */ + 0, /* nb preallocs */ + (GInstanceInitFunc) ea_calendar_cell_init, /* instance init */ + NULL /* value table */ + }; + + static const GInterfaceInfo atk_component_info = { + (GInterfaceInitFunc) atk_component_interface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + type = g_type_register_static ( + ATK_TYPE_GOBJECT_ACCESSIBLE, + "EaCalendarCell", &tinfo, 0); + g_type_add_interface_static ( + type, ATK_TYPE_COMPONENT, + &atk_component_info); + } + + return type; +} + +static void +ea_calendar_cell_class_init (EaCalendarCellClass *klass) +{ + AtkObjectClass *class = ATK_OBJECT_CLASS (klass); + +#ifdef ACC_DEBUG + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = ea_calendar_cell_finalize; +#endif + + parent_class = g_type_class_peek_parent (klass); + + class->get_name = ea_calendar_cell_get_name; + class->get_description = ea_calendar_cell_get_description; + + class->get_parent = ea_calendar_cell_get_parent; + class->get_index_in_parent = ea_calendar_cell_get_index_in_parent; + class->ref_state_set = ea_calendar_cell_ref_state_set; +} + +static void +ea_calendar_cell_init (EaCalendarCell *a11y) +{ + a11y->state_set = atk_state_set_new (); + atk_state_set_add_state (a11y->state_set, ATK_STATE_TRANSIENT); + atk_state_set_add_state (a11y->state_set, ATK_STATE_ENABLED); + atk_state_set_add_state (a11y->state_set, ATK_STATE_SENSITIVE); + atk_state_set_add_state (a11y->state_set, ATK_STATE_SELECTABLE); + atk_state_set_add_state (a11y->state_set, ATK_STATE_SHOWING); + atk_state_set_add_state (a11y->state_set, ATK_STATE_FOCUSABLE); +} + +AtkObject * +ea_calendar_cell_new (GObject *obj) +{ + gpointer object; + AtkObject *atk_object; + + g_return_val_if_fail (E_IS_CALENDAR_CELL (obj), NULL); + object = g_object_new (EA_TYPE_CALENDAR_CELL, NULL); + atk_object = ATK_OBJECT (object); + atk_object_initialize (atk_object, obj); + atk_object->role = ATK_ROLE_TABLE_CELL; + +#ifdef ACC_DEBUG + ++n_ea_calendar_cell_created; + g_print ( + "ACC_DEBUG: n_ea_calendar_cell_created = %d\n", + n_ea_calendar_cell_created); +#endif + return atk_object; +} + +#ifdef ACC_DEBUG +static void ea_calendar_cell_finalize (GObject *object) +{ + G_OBJECT_CLASS (parent_class)->finalize (object); + + ++n_ea_calendar_cell_destroyed; + g_print ( + "ACC_DEBUG: n_ea_calendar_cell_destroyed = %d\n", + n_ea_calendar_cell_destroyed); +} +#endif + +static const gchar * +ea_calendar_cell_get_name (AtkObject *accessible) +{ + GObject *g_obj; + + g_return_val_if_fail (EA_IS_CALENDAR_CELL (accessible), NULL); + + g_obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (accessible)); + if (!g_obj) + /* defunct object*/ + return NULL; + + if (!accessible->name) { + AtkObject *atk_obj; + EaCalendarItem *ea_calitem; + ECalendarCell *cell; + gint day_index; + gint year, month, day; + gchar buffer[128]; + + cell = E_CALENDAR_CELL (g_obj); + atk_obj = ea_calendar_cell_get_parent (accessible); + ea_calitem = EA_CALENDAR_ITEM (atk_obj); + day_index = atk_table_get_index_at ( + ATK_TABLE (ea_calitem), + cell->row, cell->column); + e_calendar_item_get_date_for_offset (cell->calitem, day_index, + &year, &month, &day); + + g_snprintf (buffer, 128, "%d-%d-%d", year, month + 1, day); + ATK_OBJECT_CLASS (parent_class)->set_name (accessible, buffer); + } + return accessible->name; +} + +static const gchar * +ea_calendar_cell_get_description (AtkObject *accessible) +{ + return ea_calendar_cell_get_name (accessible); +} + +static AtkObject * +ea_calendar_cell_get_parent (AtkObject *accessible) +{ + GObject *g_obj; + ECalendarCell *cell; + ECalendarItem *calitem; + + g_return_val_if_fail (EA_IS_CALENDAR_CELL (accessible), NULL); + + g_obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (accessible)); + if (!g_obj) + /* defunct object*/ + return NULL; + + cell = E_CALENDAR_CELL (g_obj); + calitem = cell->calitem; + return atk_gobject_accessible_for_object (G_OBJECT (calitem)); +} + +static gint +ea_calendar_cell_get_index_in_parent (AtkObject *accessible) +{ + GObject *g_obj; + ECalendarCell *cell; + AtkObject *parent; + + g_return_val_if_fail (EA_IS_CALENDAR_CELL (accessible), -1); + + g_obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (accessible)); + if (!g_obj) + return -1; + cell = E_CALENDAR_CELL (g_obj); + parent = atk_object_get_parent (accessible); + return atk_table_get_index_at ( + ATK_TABLE (parent), + cell->row, cell->column); +} + +static AtkStateSet * +ea_calendar_cell_ref_state_set (AtkObject *accessible) +{ + EaCalendarCell *atk_cell = EA_CALENDAR_CELL (accessible); + + g_return_val_if_fail (atk_cell->state_set, NULL); + + g_object_ref (atk_cell->state_set); + + return atk_cell->state_set; + +} + +/* Atk Component Interface */ + +static void +atk_component_interface_init (AtkComponentIface *iface) +{ + g_return_if_fail (iface != NULL); + + iface->get_extents = component_interface_get_extents; + iface->grab_focus = component_interface_grab_focus; +} + +static void +component_interface_get_extents (AtkComponent *component, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coord_type) +{ + GObject *g_obj; + AtkObject *atk_obj, *atk_canvas; + ECalendarCell *cell; + ECalendarItem *calitem; + EaCalendarItem *ea_calitem; + gint day_index; + gint year, month, day; + gint canvas_x, canvas_y, canvas_width, canvas_height; + + *x = *y = *width = *height = 0; + + g_return_if_fail (EA_IS_CALENDAR_CELL (component)); + + g_obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (component)); + if (!g_obj) + /* defunct object*/ + return; + + cell = E_CALENDAR_CELL (g_obj); + calitem = cell->calitem; + atk_obj = atk_gobject_accessible_for_object (G_OBJECT (calitem)); + ea_calitem = EA_CALENDAR_ITEM (atk_obj); + day_index = atk_table_get_index_at ( + ATK_TABLE (ea_calitem), + cell->row, cell->column); + e_calendar_item_get_date_for_offset (calitem, day_index, + &year, &month, &day); + + if (!e_calendar_item_get_day_extents (calitem, + year, month, day, + x, y, width, height)) + return; + atk_canvas = atk_object_get_parent (ATK_OBJECT (ea_calitem)); + atk_component_get_extents ( + ATK_COMPONENT (atk_canvas), + &canvas_x, &canvas_y, + &canvas_width, &canvas_height, + coord_type); + *x += canvas_x; + *y += canvas_y; +} + +static gboolean +component_interface_grab_focus (AtkComponent *component) +{ + GObject *g_obj; + GtkWidget *toplevel; + AtkObject *ea_calitem; + ECalendarItem *calitem; + EaCalendarCell *a11y; + gint index; + + a11y = EA_CALENDAR_CELL (component); + ea_calitem = ea_calendar_cell_get_parent (ATK_OBJECT (a11y)); + + g_obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (ea_calitem)); + calitem = E_CALENDAR_ITEM (g_obj); + + index = atk_object_get_index_in_parent (ATK_OBJECT (a11y)); + + atk_selection_clear_selection (ATK_SELECTION (ea_calitem)); + atk_selection_add_selection (ATK_SELECTION (ea_calitem), index); + + gtk_widget_grab_focus (GTK_WIDGET (GNOME_CANVAS_ITEM (calitem)->canvas)); + toplevel = gtk_widget_get_toplevel ( + GTK_WIDGET (GNOME_CANVAS_ITEM (calitem)->canvas)); + if (toplevel && gtk_widget_is_toplevel (toplevel)) + gtk_window_present (GTK_WINDOW (toplevel)); + + return TRUE; + +} diff --git a/e-util/ea-calendar-cell.h b/e-util/ea-calendar-cell.h new file mode 100644 index 0000000000..2d228c2a74 --- /dev/null +++ b/e-util/ea-calendar-cell.h @@ -0,0 +1,90 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __EA_CALENDAR_CELL_H__ +#define __EA_CALENDAR_CELL_H__ + +#include <atk/atkgobjectaccessible.h> +#include <e-util/e-calendar-item.h> + +G_BEGIN_DECLS + +#define E_TYPE_CALENDAR_CELL (e_calendar_cell_get_type ()) +#define E_CALENDAR_CELL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_TYPE_CALENDAR_CELL, ECalendarCell)) +#define E_CALENDAR_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), E_TYPE_CALENDAR_CELL, ECalendarCellClass)) +#define E_IS_CALENDAR_CELL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_CALENDAR_CELL)) +#define E_IS_CALENDAR_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), E_TYPE_CALENDAR_CELL)) +#define E_CALENDAR_CELL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), E_TYPE_CALENDAR_CELL, ECalendarCellClass)) + +typedef struct _ECalendarCell ECalendarCell; +typedef struct _ECalendarCellClass ECalendarCellClass; + +struct _ECalendarCell +{ + GObject parent; + ECalendarItem *calitem; + gint row; + gint column; +}; + +GType e_calendar_cell_get_type (void); + +struct _ECalendarCellClass +{ + GObjectClass parent_class; +}; + +ECalendarCell * e_calendar_cell_new (ECalendarItem *calitem, + gint row, gint column); + +#define EA_TYPE_CALENDAR_CELL (ea_calendar_cell_get_type ()) +#define EA_CALENDAR_CELL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EA_TYPE_CALENDAR_CELL, EaCalendarCell)) +#define EA_CALENDAR_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EA_TYPE_CALENDAR_CELL, EaCalendarCellClass)) +#define EA_IS_CALENDAR_CELL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EA_TYPE_CALENDAR_CELL)) +#define EA_IS_CALENDAR_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EA_TYPE_CALENDAR_CELL)) +#define EA_CALENDAR_CELL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EA_TYPE_CALENDAR_CELL, EaCalendarCellClass)) + +typedef struct _EaCalendarCell EaCalendarCell; +typedef struct _EaCalendarCellClass EaCalendarCellClass; + +struct _EaCalendarCell +{ + AtkGObjectAccessible parent; + AtkStateSet *state_set; +}; + +GType ea_calendar_cell_get_type (void); + +struct _EaCalendarCellClass +{ + AtkGObjectAccessibleClass parent_class; +}; + +AtkObject * ea_calendar_cell_new (GObject *gobj); + +G_END_DECLS + +#endif /* __EA_CALENDAR_CELL_H__ */ diff --git a/e-util/ea-calendar-item.c b/e-util/ea-calendar-item.c new file mode 100644 index 0000000000..2f5ac91d5b --- /dev/null +++ b/e-util/ea-calendar-item.c @@ -0,0 +1,1373 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <time.h> +#include <string.h> +#include <libgnomecanvas/gnome-canvas.h> +#include <glib/gi18n.h> + +#include <libedataserver/libedataserver.h> + +#include "ea-calendar-item.h" +#include "ea-calendar-cell.h" +#include "ea-cell-table.h" + +#include "e-misc-utils.h" + +#define EA_CALENDAR_COLUMN_NUM E_CALENDAR_COLS_PER_MONTH + +/* EaCalendarItem */ +static void ea_calendar_item_class_init (EaCalendarItemClass *class); +static void ea_calendar_item_finalize (GObject *object); + +static const gchar * ea_calendar_item_get_name (AtkObject *accessible); +static const gchar * ea_calendar_item_get_description (AtkObject *accessible); +static gint ea_calendar_item_get_n_children (AtkObject *accessible); +static AtkObject *ea_calendar_item_ref_child (AtkObject *accessible, gint index); +static AtkStateSet * ea_calendar_item_ref_state_set (AtkObject *accessible); + +/* atk table interface */ +static void atk_table_interface_init (AtkTableIface *iface); +static gint table_interface_get_index_at (AtkTable *table, + gint row, + gint column); +static gint table_interface_get_column_at_index (AtkTable *table, + gint index); +static gint table_interface_get_row_at_index (AtkTable *table, + gint index); +static AtkObject * table_interface_ref_at (AtkTable *table, + gint row, + gint column); +static gint table_interface_get_n_rows (AtkTable *table); +static gint table_interface_get_n_columns (AtkTable *table); +static gint table_interface_get_column_extent_at (AtkTable *table, + gint row, + gint column); +static gint table_interface_get_row_extent_at (AtkTable *table, + gint row, + gint column); + +static gboolean table_interface_is_row_selected (AtkTable *table, + gint row); +static gboolean table_interface_is_column_selected (AtkTable *table, + gint row); +static gboolean table_interface_is_selected (AtkTable *table, + gint row, + gint column); +static gint table_interface_get_selected_rows (AtkTable *table, + gint **rows_selected); +static gint table_interface_get_selected_columns (AtkTable *table, + gint **columns_selected); +static gboolean table_interface_add_row_selection (AtkTable *table, gint row); +static gboolean table_interface_remove_row_selection (AtkTable *table, + gint row); +static gboolean table_interface_add_column_selection (AtkTable *table, + gint column); +static gboolean table_interface_remove_column_selection (AtkTable *table, + gint column); +static AtkObject * table_interface_get_row_header (AtkTable *table, gint row); +static AtkObject * table_interface_get_column_header (AtkTable *table, + gint in_col); +static AtkObject * table_interface_get_caption (AtkTable *table); + +static const gchar * +table_interface_get_column_description (AtkTable *table, + gint in_col); + +static const gchar * +table_interface_get_row_description (AtkTable *table, + gint row); + +static AtkObject *table_interface_get_summary (AtkTable *table); + +/* atk selection interface */ +static void atk_selection_interface_init (AtkSelectionIface *iface); +static gboolean selection_interface_add_selection (AtkSelection *selection, + gint i); +static gboolean selection_interface_clear_selection (AtkSelection *selection); +static AtkObject *selection_interface_ref_selection (AtkSelection *selection, + gint i); +static gint selection_interface_get_selection_count (AtkSelection *selection); +static gboolean selection_interface_is_child_selected (AtkSelection *selection, + gint i); + +/* callbacks */ +static void selection_preview_change_cb (ECalendarItem *calitem); +static void date_range_changed_cb (ECalendarItem *calitem); + +/* helpers */ +static EaCellTable *ea_calendar_item_get_cell_data (EaCalendarItem *ea_calitem); +static void ea_calendar_item_destory_cell_data (EaCalendarItem *ea_calitem); +static gboolean ea_calendar_item_get_column_label (EaCalendarItem *ea_calitem, + gint column, + gchar *buffer, + gint buffer_size); +static gboolean ea_calendar_item_get_row_label (EaCalendarItem *ea_calitem, + gint row, + gchar *buffer, + gint buffer_size); +static gboolean e_calendar_item_get_offset_for_date (ECalendarItem *calitem, + gint year, + gint month, + gint day, + gint *offset); +static void ea_calendar_set_focus_object (EaCalendarItem *ea_calitem, + AtkObject *item_cell); + +#ifdef ACC_DEBUG +static gint n_ea_calendar_item_created = 0; +static gint n_ea_calendar_item_destroyed = 0; +#endif + +static gpointer parent_class = NULL; + +GType +ea_calendar_item_get_type (void) +{ + static GType type = 0; + AtkObjectFactory *factory; + GTypeQuery query; + GType derived_atk_type; + + if (!type) { + static GTypeInfo tinfo = { + sizeof (EaCalendarItemClass), + (GBaseInitFunc) NULL, /* base init */ + (GBaseFinalizeFunc) NULL, /* base finalize */ + (GClassInitFunc) ea_calendar_item_class_init, /* class init */ + (GClassFinalizeFunc) NULL, /* class finalize */ + NULL, /* class data */ + sizeof (EaCalendarItem), /* instance size */ + 0, /* nb preallocs */ + (GInstanceInitFunc) NULL, /* instance init */ + NULL /* value table */ + }; + + static const GInterfaceInfo atk_table_info = { + (GInterfaceInitFunc) atk_table_interface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + static const GInterfaceInfo atk_selection_info = { + (GInterfaceInitFunc) atk_selection_interface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + /* + * Figure out the size of the class and instance + * we are run-time deriving from (GailCanvasItem, in this case) + */ + + factory = atk_registry_get_factory ( + atk_get_default_registry (), + GNOME_TYPE_CANVAS_ITEM); + derived_atk_type = atk_object_factory_get_accessible_type (factory); + g_type_query (derived_atk_type, &query); + + tinfo.class_size = query.class_size; + tinfo.instance_size = query.instance_size; + + type = g_type_register_static ( + derived_atk_type, + "EaCalendarItem", &tinfo, 0); + g_type_add_interface_static ( + type, ATK_TYPE_TABLE, + &atk_table_info); + g_type_add_interface_static ( + type, ATK_TYPE_SELECTION, + &atk_selection_info); + } + + return type; +} + +static void +ea_calendar_item_class_init (EaCalendarItemClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + AtkObjectClass *class = ATK_OBJECT_CLASS (klass); + + gobject_class->finalize = ea_calendar_item_finalize; + parent_class = g_type_class_peek_parent (klass); + + class->get_name = ea_calendar_item_get_name; + class->get_description = ea_calendar_item_get_description; + class->ref_state_set = ea_calendar_item_ref_state_set; + + class->get_n_children = ea_calendar_item_get_n_children; + class->ref_child = ea_calendar_item_ref_child; +} + +AtkObject * +ea_calendar_item_new (GObject *obj) +{ + gpointer object; + AtkObject *atk_object; + AtkObject *item_cell; + + g_return_val_if_fail (E_IS_CALENDAR_ITEM (obj), NULL); + object = g_object_new (EA_TYPE_CALENDAR_ITEM, NULL); + atk_object = ATK_OBJECT (object); + atk_object_initialize (atk_object, obj); + atk_object->role = ATK_ROLE_CALENDAR; + + item_cell = atk_selection_ref_selection ( + ATK_SELECTION (atk_object), 0); + if (item_cell) + ea_calendar_set_focus_object (EA_CALENDAR_ITEM (atk_object), item_cell); + +#ifdef ACC_DEBUG + ++n_ea_calendar_item_created; + g_print ( + "ACC_DEBUG: n_ea_calendar_item_created = %d\n", + n_ea_calendar_item_created); +#endif + /* connect signal handlers */ + g_signal_connect ( + obj, "selection_preview_changed", + G_CALLBACK (selection_preview_change_cb), atk_object); + g_signal_connect ( + obj, "date_range_changed", + G_CALLBACK (date_range_changed_cb), atk_object); + + return atk_object; +} + +static void +ea_calendar_item_finalize (GObject *object) +{ + EaCalendarItem *ea_calitem; + + g_return_if_fail (EA_IS_CALENDAR_ITEM (object)); + + ea_calitem = EA_CALENDAR_ITEM (object); + + /* Free the allocated cell data */ + ea_calendar_item_destory_cell_data (ea_calitem); + + G_OBJECT_CLASS (parent_class)->finalize (object); +#ifdef ACC_DEBUG + ++n_ea_calendar_item_destroyed; + printf ( + "ACC_DEBUG: n_ea_calendar_item_destroyed = %d\n", + n_ea_calendar_item_destroyed); +#endif +} + +static const gchar * +ea_calendar_item_get_name (AtkObject *accessible) +{ + GObject *g_obj; + ECalendarItem *calitem; + gint start_year, start_month, start_day; + gint end_year, end_month, end_day; + gchar *name_str = NULL; + gchar buffer_start[128] = ""; + gchar buffer_end[128] = ""; + struct tm day_start = { 0 }; + struct tm day_end = { 0 }; + + g_return_val_if_fail (EA_IS_CALENDAR_ITEM (accessible), NULL); + + g_obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (accessible)); + if (!g_obj) + return NULL; + g_return_val_if_fail (E_IS_CALENDAR_ITEM (g_obj), NULL); + + calitem = E_CALENDAR_ITEM (g_obj); + if (e_calendar_item_get_date_range ( + calitem, + &start_year, &start_month, &start_day, + &end_year, &end_month, &end_day)) { + + day_start.tm_year = start_year - 1900; + day_start.tm_mon = start_month; + day_start.tm_mday = start_day; + day_start.tm_isdst = -1; + + e_utf8_strftime ( + buffer_start, sizeof (buffer_start), + _("%d %B %Y"), &day_start); + + day_end.tm_year = end_year - 1900; + day_end.tm_mon = end_month; + day_end.tm_mday = end_day; + day_end.tm_isdst = -1; + + e_utf8_strftime ( + buffer_end, sizeof (buffer_end), + _("%d %B %Y"), &day_end); + + name_str = g_strdup_printf ( + _("Calendar: from %s to %s"), + buffer_start, buffer_end); + } + +#if 0 + if (e_calendar_item_get_selection (calitem, &select_start, &select_end)) { + GDate select_start, select_end; + gint year1, year2, month1, month2, day1, day2; + + year1 = g_date_get_year (&select_start); + month1 = g_date_get_month (&select_start); + day1 = g_date_get_day (&select_start); + + year2 = g_date_get_year (&select_end); + month2 = g_date_get_month (&select_end); + day2 = g_date_get_day (&select_end); + + sprintf ( + new_name + strlen (new_name), + " : current selection: from %d-%d-%d to %d-%d-%d.", + year1, month1, day1, + year2, month2, day2); + } +#endif + + ATK_OBJECT_CLASS (parent_class)->set_name (accessible, name_str); + g_free (name_str); + + return accessible->name; +} + +static const gchar * +ea_calendar_item_get_description (AtkObject *accessible) +{ + if (accessible->description) + return accessible->description; + + return _("evolution calendar item"); +} + +static AtkStateSet * +ea_calendar_item_ref_state_set (AtkObject *accessible) +{ + AtkStateSet *state_set; + GObject *g_obj; + + state_set = ATK_OBJECT_CLASS (parent_class)->ref_state_set (accessible); + g_obj = atk_gobject_accessible_get_object ( + ATK_GOBJECT_ACCESSIBLE (accessible)); + if (!g_obj) + return state_set; + + atk_state_set_add_state (state_set, ATK_STATE_ENABLED); + atk_state_set_add_state (state_set, ATK_STATE_SENSITIVE); + + return state_set; +} + +static gint +ea_calendar_item_get_n_children (AtkObject *accessible) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + ECalendarItem *calitem; + gint n_children = 0; + gint start_year, start_month, start_day; + gint end_year, end_month, end_day; + GDate *start_date, *end_date; + + g_return_val_if_fail (EA_IS_CALENDAR_ITEM (accessible), -1); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (accessible); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return -1; + + calitem = E_CALENDAR_ITEM (g_obj); + if (!e_calendar_item_get_date_range (calitem, &start_year, + &start_month, &start_day, + &end_year, &end_month, + &end_day)) + return 0; + + start_date = g_date_new_dmy (start_day, start_month + 1, start_year); + end_date = g_date_new_dmy (end_day, end_month + 1, end_year); + + n_children = g_date_days_between (start_date, end_date) + 1; + g_free (start_date); + g_free (end_date); + return n_children; +} + +static AtkObject * +ea_calendar_item_ref_child (AtkObject *accessible, + gint index) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + ECalendarItem *calitem; + gint n_children; + ECalendarCell *cell; + EaCellTable *cell_data; + EaCalendarItem *ea_calitem; + + g_return_val_if_fail (EA_IS_CALENDAR_ITEM (accessible), NULL); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (accessible); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return NULL; + + calitem = E_CALENDAR_ITEM (g_obj); + + n_children = ea_calendar_item_get_n_children (accessible); + if (index < 0 || index >= n_children) + return NULL; + + ea_calitem = EA_CALENDAR_ITEM (accessible); + cell_data = ea_calendar_item_get_cell_data (ea_calitem); + if (!cell_data) + return NULL; + + cell = ea_cell_table_get_cell_at_index (cell_data, index); + if (!cell) { + cell = e_calendar_cell_new ( + calitem, + index / EA_CALENDAR_COLUMN_NUM, + index % EA_CALENDAR_COLUMN_NUM); + ea_cell_table_set_cell_at_index (cell_data, index, cell); + g_object_unref (cell); + } + +#ifdef ACC_DEBUG + g_print ( + "AccDebug: ea_calendar_item children[%d]=%p\n", index, + (gpointer) cell); +#endif + return g_object_ref (atk_gobject_accessible_for_object (G_OBJECT (cell))); +} + +/* atk table interface */ + +static void +atk_table_interface_init (AtkTableIface *iface) +{ + g_return_if_fail (iface != NULL); + + iface->ref_at = table_interface_ref_at; + + iface->get_n_rows = table_interface_get_n_rows; + iface->get_n_columns = table_interface_get_n_columns; + iface->get_index_at = table_interface_get_index_at; + iface->get_column_at_index = table_interface_get_column_at_index; + iface->get_row_at_index = table_interface_get_row_at_index; + iface->get_column_extent_at = table_interface_get_column_extent_at; + iface->get_row_extent_at = table_interface_get_row_extent_at; + + iface->is_selected = table_interface_is_selected; + iface->get_selected_rows = table_interface_get_selected_rows; + iface->get_selected_columns = table_interface_get_selected_columns; + iface->is_row_selected = table_interface_is_row_selected; + iface->is_column_selected = table_interface_is_column_selected; + iface->add_row_selection = table_interface_add_row_selection; + iface->remove_row_selection = table_interface_remove_row_selection; + iface->add_column_selection = table_interface_add_column_selection; + iface->remove_column_selection = table_interface_remove_column_selection; + + iface->get_row_header = table_interface_get_row_header; + iface->get_column_header = table_interface_get_column_header; + iface->get_caption = table_interface_get_caption; + iface->get_summary = table_interface_get_summary; + iface->get_row_description = table_interface_get_row_description; + iface->get_column_description = table_interface_get_column_description; +} + +static AtkObject * +table_interface_ref_at (AtkTable *table, + gint row, + gint column) +{ + gint index; + + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + index = EA_CALENDAR_COLUMN_NUM * row + column; + return ea_calendar_item_ref_child (ATK_OBJECT (ea_calitem), index); +} + +static gint +table_interface_get_n_rows (AtkTable *table) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + gint n_children; + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return -1; + + n_children = ea_calendar_item_get_n_children (ATK_OBJECT (ea_calitem)); + return (n_children - 1) / EA_CALENDAR_COLUMN_NUM + 1; +} + +static gint +table_interface_get_n_columns (AtkTable *table) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return -1; + + return EA_CALENDAR_COLUMN_NUM; +} + +static gint +table_interface_get_index_at (AtkTable *table, + gint row, + gint column) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return -1; + + return row * EA_CALENDAR_COLUMN_NUM + column; +} + +static gint +table_interface_get_column_at_index (AtkTable *table, + gint index) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + gint n_children; + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return -1; + + n_children = ea_calendar_item_get_n_children (ATK_OBJECT (ea_calitem)); + if (index >= 0 && index < n_children) + return index % EA_CALENDAR_COLUMN_NUM; + return -1; +} + +static gint +table_interface_get_row_at_index (AtkTable *table, + gint index) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + gint n_children; + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return -1; + + n_children = ea_calendar_item_get_n_children (ATK_OBJECT (ea_calitem)); + if (index >= 0 && index < n_children) + return index / EA_CALENDAR_COLUMN_NUM; + return -1; +} + +static gint +table_interface_get_column_extent_at (AtkTable *table, + gint row, + gint column) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + ECalendarItem *calitem; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return FALSE; + + calitem = E_CALENDAR_ITEM (g_obj); + return calitem->cell_width; +} + +static gint +table_interface_get_row_extent_at (AtkTable *table, + gint row, + gint column) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + ECalendarItem *calitem; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return FALSE; + + calitem = E_CALENDAR_ITEM (g_obj); + return calitem->cell_height; +} + +/* any day in the row is selected, the row is selected */ +static gboolean +table_interface_is_row_selected (AtkTable *table, + gint row) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + gint n_rows; + ECalendarItem *calitem; + gint row_index_start, row_index_end; + gint sel_index_start, sel_index_end; + + GDate start_date, end_date; + + g_return_val_if_fail (EA_IS_CALENDAR_ITEM (table), FALSE); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (table); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return FALSE; + + n_rows = table_interface_get_n_rows (table); + if (row < 0 || row >= n_rows) + return FALSE; + + row_index_start = row * EA_CALENDAR_COLUMN_NUM; + row_index_end = row_index_start + EA_CALENDAR_COLUMN_NUM - 1; + + calitem = E_CALENDAR_ITEM (g_obj); + if (!e_calendar_item_get_selection (calitem, &start_date, &end_date)) + return FALSE; + + e_calendar_item_get_offset_for_date (calitem, + g_date_get_year (&start_date), + g_date_get_month (&start_date), + g_date_get_day (&start_date), + &sel_index_start); + e_calendar_item_get_offset_for_date (calitem, + g_date_get_year (&end_date), + g_date_get_month (&end_date), + g_date_get_day (&end_date), + &sel_index_end); + + if ((sel_index_start < row_index_start && + sel_index_end >= row_index_start) || + (sel_index_start >= row_index_start && + sel_index_start <= row_index_end)) + return TRUE; + return FALSE; +} + +static gboolean +table_interface_is_selected (AtkTable *table, + gint row, + gint column) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + gint n_rows, n_columns; + ECalendarItem *calitem; + gint index; + gint sel_index_start, sel_index_end; + + GDate start_date, end_date; + + g_return_val_if_fail (EA_IS_CALENDAR_ITEM (table), FALSE); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (table); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return FALSE; + + n_rows = table_interface_get_n_rows (table); + if (row < 0 || row >= n_rows) + return FALSE; + n_columns = table_interface_get_n_columns (table); + if (column < 0 || column >= n_columns) + return FALSE; + + index = table_interface_get_index_at (table, row, column); + + calitem = E_CALENDAR_ITEM (g_obj); + if (!e_calendar_item_get_selection (calitem, &start_date, &end_date)) + return FALSE; + + e_calendar_item_get_offset_for_date (calitem, + g_date_get_year (&start_date), + g_date_get_month (&start_date), + g_date_get_day (&start_date), + &sel_index_start); + e_calendar_item_get_offset_for_date (calitem, + g_date_get_year (&end_date), + g_date_get_month (&end_date), + g_date_get_day (&end_date), &sel_index_end); + + if (sel_index_start <= index && sel_index_end >= index) + return TRUE; + return FALSE; +} + +static gboolean +table_interface_is_column_selected (AtkTable *table, + gint column) +{ + return FALSE; +} + +static gint +table_interface_get_selected_rows (AtkTable *table, + gint **rows_selected) +{ + *rows_selected = NULL; + return -1; +} + +static gint +table_interface_get_selected_columns (AtkTable *table, + gint **columns_selected) +{ + *columns_selected = NULL; + return -1; +} + +static gboolean +table_interface_add_row_selection (AtkTable *table, + gint row) +{ + return FALSE; +} + +static gboolean +table_interface_remove_row_selection (AtkTable *table, + gint row) +{ + return FALSE; +} + +static gboolean +table_interface_add_column_selection (AtkTable *table, + gint column) +{ + return FALSE; +} + +static gboolean +table_interface_remove_column_selection (AtkTable *table, + gint column) +{ + /* FIXME: NOT IMPLEMENTED */ + return FALSE; +} + +static AtkObject * +table_interface_get_row_header (AtkTable *table, + gint row) +{ + /* FIXME: NOT IMPLEMENTED */ + return NULL; +} + +static AtkObject * +table_interface_get_column_header (AtkTable *table, + gint in_col) +{ + /* FIXME: NOT IMPLEMENTED */ + return NULL; +} + +static AtkObject * +table_interface_get_caption (AtkTable *table) +{ + /* FIXME: NOT IMPLEMENTED */ + return NULL; +} + +static const gchar * +table_interface_get_column_description (AtkTable *table, + gint in_col) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + const gchar *description = NULL; + EaCellTable *cell_data; + gint n_columns; + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return NULL; + + n_columns = table_interface_get_n_columns (table); + if (in_col < 0 || in_col >= n_columns) + return NULL; + cell_data = ea_calendar_item_get_cell_data (ea_calitem); + if (!cell_data) + return NULL; + + description = ea_cell_table_get_column_label (cell_data, in_col); + if (!description) { + gchar buffer[128] = "column description"; + ea_calendar_item_get_column_label ( + ea_calitem, in_col, + buffer, sizeof (buffer)); + ea_cell_table_set_column_label (cell_data, in_col, buffer); + description = ea_cell_table_get_column_label ( + cell_data, in_col); + } + return description; +} + +static const gchar * +table_interface_get_row_description (AtkTable *table, + gint row) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (table); + const gchar *description = NULL; + EaCellTable *cell_data; + gint n_rows; + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return NULL; + + n_rows = table_interface_get_n_rows (table); + if (row < 0 || row >= n_rows) + return NULL; + cell_data = ea_calendar_item_get_cell_data (ea_calitem); + if (!cell_data) + return NULL; + + description = ea_cell_table_get_row_label (cell_data, row); + if (!description) { + gchar buffer[128] = "row description"; + ea_calendar_item_get_row_label ( + ea_calitem, row, + buffer, sizeof (buffer)); + ea_cell_table_set_row_label (cell_data, row, buffer); + description = ea_cell_table_get_row_label ( + cell_data, + row); + } + return description; +} + +static AtkObject * +table_interface_get_summary (AtkTable *table) +{ + /* FIXME: NOT IMPLEMENTED */ + return NULL; +} + +/* atkselection interface */ + +static void +atk_selection_interface_init (AtkSelectionIface *iface) +{ + g_return_if_fail (iface != NULL); + + iface->add_selection = selection_interface_add_selection; + iface->clear_selection = selection_interface_clear_selection; + iface->ref_selection = selection_interface_ref_selection; + iface->get_selection_count = selection_interface_get_selection_count; + iface->is_child_selected = selection_interface_is_child_selected; +} + +static gboolean +selection_interface_add_selection (AtkSelection *selection, + gint index) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + ECalendarItem *calitem; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (selection); + gint year, month, day; + GDate start_date, end_date; + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return FALSE; + + calitem = E_CALENDAR_ITEM (g_obj); + if (!e_calendar_item_get_date_for_offset (calitem, index, + &year, &month, &day)) + return FALSE; + + /* FIXME: not support mulit-selection */ + g_date_set_dmy (&start_date, day, month + 1, year); + end_date = start_date; + e_calendar_item_set_selection (calitem, &start_date, &end_date); + return TRUE; +} + +static gboolean +selection_interface_clear_selection (AtkSelection *selection) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + ECalendarItem *calitem; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (selection); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return FALSE; + + calitem = E_CALENDAR_ITEM (g_obj); + e_calendar_item_set_selection (calitem, NULL, NULL); + + return TRUE; +} + +static AtkObject * +selection_interface_ref_selection (AtkSelection *selection, + gint i) +{ + GObject *g_obj; + ECalendarItem *calitem; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (selection); + gint count, sel_offset; + GDate start_date, end_date; + + count = selection_interface_get_selection_count (selection); + if (i < 0 || i >= count) + return NULL; + + g_obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (ea_calitem)); + + calitem = E_CALENDAR_ITEM (g_obj); + if (!e_calendar_item_get_selection (calitem, &start_date, &end_date)) + return NULL; + if (!e_calendar_item_get_offset_for_date (calitem, + g_date_get_year (&start_date), + g_date_get_month (&start_date) - 1, + g_date_get_day (&start_date), + &sel_offset)) + return NULL; + + return ea_calendar_item_ref_child (ATK_OBJECT (selection), sel_offset + i); +} + +static gint +selection_interface_get_selection_count (AtkSelection *selection) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + ECalendarItem *calitem; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (selection); + GDate start_date, end_date; + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return 0; + + calitem = E_CALENDAR_ITEM (g_obj); + if (e_calendar_item_get_selection (calitem, &start_date, &end_date)) + return g_date_days_between (&start_date, &end_date) + 1; + else + return 0; +} + +static gboolean +selection_interface_is_child_selected (AtkSelection *selection, + gint index) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + EaCalendarItem * ea_calitem = EA_CALENDAR_ITEM (selection); + gint row, column, n_children; + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return FALSE; + + n_children = atk_object_get_n_accessible_children (ATK_OBJECT (selection)); + if (index < 0 || index >= n_children) + return FALSE; + + row = index / EA_CALENDAR_COLUMN_NUM; + column = index % EA_CALENDAR_COLUMN_NUM; + + return table_interface_is_selected (ATK_TABLE (selection), row, column); +} + +/* callbacks */ + +static void +selection_preview_change_cb (ECalendarItem *calitem) +{ + AtkObject *atk_obj; + AtkObject *item_cell; + + g_return_if_fail (E_IS_CALENDAR_ITEM (calitem)); + atk_obj = atk_gobject_accessible_for_object (G_OBJECT (calitem)); + ea_calendar_item_destory_cell_data (EA_CALENDAR_ITEM (atk_obj)); + + /* only deal with the first selected child, for now */ + item_cell = atk_selection_ref_selection ( + ATK_SELECTION (atk_obj), 0); + + if (item_cell) + ea_calendar_set_focus_object (EA_CALENDAR_ITEM (atk_obj), item_cell); + + g_signal_emit_by_name ( + atk_obj, + "active-descendant-changed", + item_cell); + g_signal_emit_by_name (atk_obj, "selection_changed"); +} + +static void +date_range_changed_cb (ECalendarItem *calitem) +{ + AtkObject *atk_obj; + AtkObject *item_cell; + + g_return_if_fail (E_IS_CALENDAR_ITEM (calitem)); + atk_obj = atk_gobject_accessible_for_object (G_OBJECT (calitem)); + ea_calendar_item_destory_cell_data (EA_CALENDAR_ITEM (atk_obj)); + + item_cell = atk_selection_ref_selection ( + ATK_SELECTION (atk_obj), 0); + if (item_cell) + ea_calendar_set_focus_object (EA_CALENDAR_ITEM (atk_obj), item_cell); + + g_signal_emit_by_name (atk_obj, "model_changed"); +} + +/* helpers */ + +static EaCellTable * +ea_calendar_item_get_cell_data (EaCalendarItem *ea_calitem) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + EaCellTable *cell_data; + + g_return_val_if_fail (ea_calitem, NULL); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return NULL; + + cell_data = g_object_get_data ( + G_OBJECT (ea_calitem), + "ea-calendar-cell-table"); + + if (!cell_data) { + gint n_cells = ea_calendar_item_get_n_children (ATK_OBJECT (ea_calitem)); + cell_data = ea_cell_table_create ( + n_cells / EA_CALENDAR_COLUMN_NUM, + EA_CALENDAR_COLUMN_NUM, + FALSE); + g_object_set_data ( + G_OBJECT (ea_calitem), + "ea-calendar-cell-table", cell_data); + } + return cell_data; +} + +static void +ea_calendar_item_destory_cell_data (EaCalendarItem *ea_calitem) +{ + EaCellTable *cell_data; + + g_return_if_fail (ea_calitem); + + cell_data = g_object_get_data ( + G_OBJECT (ea_calitem), + "ea-calendar-cell-table"); + if (cell_data) { + g_object_set_data ( + G_OBJECT (ea_calitem), + "ea-calendar-cell-table", NULL); + ea_cell_table_destroy (cell_data); + } +} + +static gboolean +ea_calendar_item_get_row_label (EaCalendarItem *ea_calitem, + gint row, + gchar *buffer, + gint buffer_size) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + ECalendarItem *calitem; + gint index, week_num; + gint year, month, day; + + g_return_val_if_fail (ea_calitem, FALSE); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return FALSE; + + calitem = E_CALENDAR_ITEM (g_obj); + + index = atk_table_get_index_at (ATK_TABLE (ea_calitem), row, 0); + if (!e_calendar_item_get_date_for_offset (calitem, index, + &year, &month, &day)) + return FALSE; + + week_num = e_calendar_item_get_week_number ( + calitem, day, month, year); + + g_snprintf (buffer, buffer_size, "week number : %d", week_num); + return TRUE; +} + +static gboolean +ea_calendar_item_get_column_label (EaCalendarItem *ea_calitem, + gint column, + gchar *buffer, + gint buffer_size) +{ + AtkGObjectAccessible *atk_gobj; + GObject *g_obj; + const gchar *abbr_name; + + g_return_val_if_fail (ea_calitem, FALSE); + + atk_gobj = ATK_GOBJECT_ACCESSIBLE (ea_calitem); + g_obj = atk_gobject_accessible_get_object (atk_gobj); + if (!g_obj) + return FALSE; + + /* Columns are 0 = Monday ... 6 = Sunday */ + abbr_name = e_get_weekday_name (column + 1, TRUE); + g_strlcpy (buffer, abbr_name, buffer_size); + + return TRUE; +} + +/* the coordinate the e-calendar canvas coord */ +gboolean +e_calendar_item_get_day_extents (ECalendarItem *calitem, + gint year, + gint month, + gint date, + gint *x, + gint *y, + gint *width, + gint *height) +{ + GnomeCanvasItem *item; + GtkWidget *widget; + GtkStyle *style; + PangoFontDescription *font_desc; + PangoContext *pango_context; + PangoFontMetrics *font_metrics; + gint char_height, xthickness, ythickness, text_y; + gint new_year, new_month, num_months, months_offset; + gint month_x, month_y, month_cell_x, month_cell_y; + gint month_row, month_col; + gint day_row, day_col; + gint days_from_week_start; + + g_return_val_if_fail (E_IS_CALENDAR_ITEM (calitem), FALSE); + + item = GNOME_CANVAS_ITEM (calitem); + widget = GTK_WIDGET (item->canvas); + style = gtk_widget_get_style (widget); + + /* Set up Pango prerequisites */ + font_desc = calitem->font_desc; + if (!font_desc) + font_desc = style->font_desc; + pango_context = gtk_widget_get_pango_context (widget); + font_metrics = pango_context_get_metrics ( + pango_context, font_desc, + pango_context_get_language (pango_context)); + + char_height = + PANGO_PIXELS (pango_font_metrics_get_ascent (font_metrics)) + + PANGO_PIXELS (pango_font_metrics_get_descent (font_metrics)); + + xthickness = style->xthickness; + ythickness = style->ythickness; + + new_year = year; + new_month = month; + e_calendar_item_normalize_date (calitem, &new_year, &new_month); + num_months = calitem->rows * calitem->cols; + months_offset = (new_year - calitem->year) * 12 + + new_month - calitem->month; + + if (months_offset > num_months || months_offset < 0) + return FALSE; + + month_row = months_offset / calitem->cols; + month_col = months_offset % calitem->cols; + + month_x = item->x1 + xthickness + calitem->x_offset + + month_col * calitem->month_width; + month_y = item->y1 + ythickness + month_row * calitem->month_height; + + month_cell_x = month_x + E_CALENDAR_ITEM_XPAD_BEFORE_WEEK_NUMBERS + + calitem->month_lpad + E_CALENDAR_ITEM_XPAD_BEFORE_CELLS; + text_y = month_y + ythickness * 2 + + E_CALENDAR_ITEM_YPAD_ABOVE_MONTH_NAME + + char_height + E_CALENDAR_ITEM_YPAD_BELOW_MONTH_NAME + + E_CALENDAR_ITEM_YPAD_ABOVE_DAY_LETTERS + calitem->month_tpad; + + month_cell_y = text_y + char_height + + E_CALENDAR_ITEM_YPAD_BELOW_DAY_LETTERS + 1 + + E_CALENDAR_ITEM_YPAD_ABOVE_CELLS; + + days_from_week_start = e_calendar_item_get_n_days_from_week_start ( + calitem, new_year, new_month); + day_row = (date + days_from_week_start - 1) / EA_CALENDAR_COLUMN_NUM; + day_col = (date + days_from_week_start - 1) % EA_CALENDAR_COLUMN_NUM; + + *x = month_cell_x + day_col * calitem->cell_width; + *y = month_cell_y + day_row * calitem->cell_height; + *width = calitem->cell_width; + *height = calitem->cell_height; + + return TRUE; +} + +/* month is from 0 to 11 */ +gboolean +e_calendar_item_get_date_for_offset (ECalendarItem *calitem, + gint day_offset, + gint *year, + gint *month, + gint *day) +{ + gint start_year, start_month, start_day; + gint end_year, end_month, end_day; + GDate *start_date; + + g_return_val_if_fail (E_IS_CALENDAR_ITEM (calitem), FALSE); + + if (!e_calendar_item_get_date_range (calitem, &start_year, + &start_month, &start_day, + &end_year, &end_month, + &end_day)) + return FALSE; + + start_date = g_date_new_dmy (start_day, start_month + 1, start_year); + + g_date_add_days (start_date, day_offset); + + *year = g_date_get_year (start_date); + *month = g_date_get_month (start_date) - 1; + *day = g_date_get_day (start_date); + + return TRUE; +} + +/* the arg month is from 0 to 11 */ +static gboolean +e_calendar_item_get_offset_for_date (ECalendarItem *calitem, + gint year, + gint month, + gint day, + gint *offset) +{ + gint start_year, start_month, start_day; + gint end_year, end_month, end_day; + GDate *start_date, *end_date; + + *offset = 0; + g_return_val_if_fail (E_IS_CALENDAR_ITEM (calitem), FALSE); + + if (!e_calendar_item_get_date_range (calitem, &start_year, + &start_month, &start_day, + &end_year, &end_month, + &end_day)) + return FALSE; + + start_date = g_date_new_dmy (start_day, start_month + 1, start_year); + end_date = g_date_new_dmy (day, month + 1, year); + + *offset = g_date_days_between (start_date, end_date); + g_free (start_date); + g_free (end_date); + + return TRUE; +} + +gint +e_calendar_item_get_n_days_from_week_start (ECalendarItem *calitem, + gint year, + gint month) +{ + struct tm tmp_tm; + gint start_weekday, days_from_week_start; + + memset (&tmp_tm, 0, sizeof (tmp_tm)); + tmp_tm.tm_year = year - 1900; + tmp_tm.tm_mon = month; + tmp_tm.tm_mday = 1; + tmp_tm.tm_isdst = -1; + mktime (&tmp_tm); + start_weekday = (tmp_tm.tm_wday + 6) % 7; /* 0 to 6 */ + days_from_week_start = (start_weekday + 7 - calitem->week_start_day) + % 7; + return days_from_week_start; +} + +static void +ea_calendar_set_focus_object (EaCalendarItem *ea_calitem, + AtkObject *item_cell) +{ + AtkStateSet *state_set, *old_state_set; + AtkObject *old_cell; + + old_cell = (AtkObject *) g_object_get_data ( + G_OBJECT (ea_calitem), "gail-focus-object"); + if (old_cell && EA_IS_CALENDAR_CELL (old_cell)) { + old_state_set = atk_object_ref_state_set (old_cell); + atk_state_set_remove_state (old_state_set, ATK_STATE_FOCUSED); + g_object_unref (old_state_set); + } + if (old_cell) + g_object_unref (old_cell); + + state_set = atk_object_ref_state_set (item_cell); + atk_state_set_add_state (state_set, ATK_STATE_FOCUSED); + g_object_set_data (G_OBJECT (ea_calitem), "gail-focus-object", item_cell); + g_object_unref (state_set); +} diff --git a/e-util/ea-calendar-item.h b/e-util/ea-calendar-item.h new file mode 100644 index 0000000000..db2e342020 --- /dev/null +++ b/e-util/ea-calendar-item.h @@ -0,0 +1,71 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __EA_CALENDAR_ITEM_H__ +#define __EA_CALENDAR_ITEM_H__ + +#include <atk/atkgobjectaccessible.h> +#include <e-util/e-calendar-item.h> + +G_BEGIN_DECLS + +#define EA_TYPE_CALENDAR_ITEM (ea_calendar_item_get_type ()) +#define EA_CALENDAR_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EA_TYPE_CALENDAR_ITEM, EaCalendarItem)) +#define EA_CALENDAR_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EA_TYPE_CALENDAR_ITEM, EaCalendarItemClass)) +#define EA_IS_CALENDAR_ITEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EA_TYPE_CALENDAR_ITEM)) +#define EA_IS_CALENDAR_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EA_TYPE_CALENDAR_ITEM)) +#define EA_CALENDAR_ITEM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EA_TYPE_CALENDAR_ITEM, EaCalendarItemClass)) + +typedef struct _EaCalendarItem EaCalendarItem; +typedef struct _EaCalendarItemClass EaCalendarItemClass; + +struct _EaCalendarItem +{ + AtkGObjectAccessible parent; +}; + +GType ea_calendar_item_get_type (void); + +struct _EaCalendarItemClass +{ + AtkGObjectAccessibleClass parent_class; +}; + +AtkObject *ea_calendar_item_new (GObject *obj); +gboolean e_calendar_item_get_day_extents (ECalendarItem *calitem, + gint year, gint month, gint date, + gint *x, gint *y, + gint *width, gint *height); +gboolean e_calendar_item_get_date_for_offset (ECalendarItem *calitem, + gint day_offset, + gint *year, gint *month, + gint *day); +gint e_calendar_item_get_n_days_from_week_start (ECalendarItem *calitem, + gint year, gint month); + +G_END_DECLS + +#endif /* __EA_CALENDAR_ITEM_H__ */ diff --git a/e-util/ea-cell-table.c b/e-util/ea-cell-table.c new file mode 100644 index 0000000000..bbdef0aea1 --- /dev/null +++ b/e-util/ea-cell-table.c @@ -0,0 +1,215 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "ea-cell-table.h" + +EaCellTable * +ea_cell_table_create (gint rows, + gint columns, + gboolean column_first) +{ + EaCellTable *cell_data; + gint index; + + g_return_val_if_fail (((columns > 0) && (rows > 0)), NULL); + + cell_data = g_new0 (EaCellTable, 1); + + cell_data->column_first = column_first; + cell_data->columns = columns; + cell_data->rows = rows; + + cell_data->column_labels = g_new0 (gchar *, columns); + for (index = columns -1; index >= 0; --index) + cell_data->column_labels[index] = NULL; + + cell_data->row_labels = g_new0 (gchar *, rows); + for (index = rows -1; index >= 0; --index) + cell_data->row_labels[index] = NULL; + + cell_data->cells = g_new0 (gpointer, (columns * rows)); + for (index = (columns * rows) -1; index >= 0; --index) + cell_data->cells[index] = NULL; + return cell_data; +} + +void +ea_cell_table_destroy (EaCellTable *cell_data) +{ + gint index; + g_return_if_fail (cell_data); + + for (index = 0; index < cell_data->columns; ++index) + if (cell_data->column_labels[index]) + g_free (cell_data->column_labels[index]); + g_free (cell_data->column_labels); + + for (index = 0; index < cell_data->rows; ++index) + if (cell_data->row_labels[index]) + g_free (cell_data->row_labels[index]); + g_free (cell_data->row_labels); + + for (index = (cell_data->columns * cell_data->rows) -1; + index >= 0; --index) + if (cell_data->cells[index] && + G_IS_OBJECT (cell_data->cells[index])) + g_object_unref (cell_data->cells[index]); + + g_free (cell_data->cells); +} + +gpointer +ea_cell_table_get_cell (EaCellTable *cell_data, + gint row, + gint column) +{ + gint index; + + g_return_val_if_fail (cell_data, NULL); + + index = ea_cell_table_get_index (cell_data, column, row); + if (index == -1) + return NULL; + + return cell_data->cells[index]; +} + +gboolean +ea_cell_table_set_cell (EaCellTable *cell_data, + gint row, + gint column, + gpointer cell) +{ + gint index; + + g_return_val_if_fail (cell_data, FALSE); + + index = ea_cell_table_get_index (cell_data, column, row); + if (index == -1) + return FALSE; + + if (cell && G_IS_OBJECT (cell)) + g_object_ref (cell); + if (cell_data->cells[index] && + G_IS_OBJECT (cell_data->cells[index])) + g_object_unref (cell_data->cells[index]); + cell_data->cells[index] = cell; + + return TRUE; +} + +gpointer +ea_cell_table_get_cell_at_index (EaCellTable *cell_data, + gint index) +{ + g_return_val_if_fail (cell_data, NULL); + + if (index >=0 && index < (cell_data->columns * cell_data->rows)) + return cell_data->cells[index]; + return NULL; +} + +gboolean +ea_cell_table_set_cell_at_index (EaCellTable *cell_data, + gint index, + gpointer cell) +{ + g_return_val_if_fail (cell_data, FALSE); + + if (index < 0 || index >=cell_data->columns * cell_data->rows) + return FALSE; + + if (cell && G_IS_OBJECT (cell)) + g_object_ref (cell); + if (cell_data->cells[index] && + G_IS_OBJECT (cell_data->cells[index])) + g_object_unref (cell_data->cells[index]); + cell_data->cells[index] = cell; + + return TRUE; +} + +const gchar * +ea_cell_table_get_column_label (EaCellTable *cell_data, + gint column) +{ + g_return_val_if_fail (cell_data, NULL); + g_return_val_if_fail ((column >= 0 && column < cell_data->columns), NULL); + + return cell_data->column_labels[column]; +} + +void +ea_cell_table_set_column_label (EaCellTable *cell_data, + gint column, + const gchar *label) +{ + g_return_if_fail (cell_data); + g_return_if_fail ((column >= 0 && column < cell_data->columns)); + + if (cell_data->column_labels[column]) + g_free (cell_data->column_labels[column]); + cell_data->column_labels[column] = g_strdup (label); +} + +const gchar * +ea_cell_table_get_row_label (EaCellTable *cell_data, + gint row) +{ + g_return_val_if_fail (cell_data, NULL); + g_return_val_if_fail ((row >= 0 && row < cell_data->rows), NULL); + + return cell_data->row_labels[row]; +} + +void +ea_cell_table_set_row_label (EaCellTable *cell_data, + gint row, + const gchar *label) +{ + g_return_if_fail (cell_data); + g_return_if_fail ((row >= 0 && row < cell_data->rows)); + + if (cell_data->row_labels[row]) + g_free (cell_data->row_labels[row]); + cell_data->row_labels[row] = g_strdup (label); +} + +gint +ea_cell_table_get_index (EaCellTable *cell_data, + gint row, + gint column) +{ + g_return_val_if_fail (cell_data, -1); + if (row < 0 || row >= cell_data->rows || + column < 0 || column >= cell_data->columns) + return -1; + + if (cell_data->column_first) + return column * cell_data->rows + row; + else + return row * cell_data->columns + column; +} diff --git a/e-util/ea-cell-table.h b/e-util/ea-cell-table.h new file mode 100644 index 0000000000..3ddd74914c --- /dev/null +++ b/e-util/ea-cell-table.h @@ -0,0 +1,63 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +/* EaCellTable */ + +#include <glib-object.h> + +struct _EaCellTable { + gint columns; + gint rows; + gboolean column_first; /* index order */ + gchar **column_labels; + gchar **row_labels; + gpointer *cells; +}; + +typedef struct _EaCellTable EaCellTable; + +EaCellTable * ea_cell_table_create (gint rows, gint columns, + gboolean column_first); +void ea_cell_table_destroy (EaCellTable * cell_data); +gpointer ea_cell_table_get_cell (EaCellTable * cell_data, + gint row, gint column); +gboolean ea_cell_table_set_cell (EaCellTable * cell_data, + gint row, gint column, gpointer cell); +gpointer ea_cell_table_get_cell_at_index (EaCellTable * cell_data, + gint index); +gboolean ea_cell_table_set_cell_at_index (EaCellTable * cell_data, + gint index, gpointer cell); + +const gchar * +ea_cell_table_get_column_label (EaCellTable * cell_data, gint column); +void ea_cell_table_set_column_label (EaCellTable * cell_data, + gint column, const gchar *label); +const gchar * +ea_cell_table_get_row_label (EaCellTable * cell_data, gint row); +void ea_cell_table_set_row_label (EaCellTable * cell_data, + gint row, const gchar *label); +gint ea_cell_table_get_index (EaCellTable *cell_data, + gint row, gint column); diff --git a/e-util/ea-factory.h b/e-util/ea-factory.h new file mode 100644 index 0000000000..c24469721d --- /dev/null +++ b/e-util/ea-factory.h @@ -0,0 +1,118 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +/* Evolution Accessibility +*/ + +#ifndef _EA_FACTORY_H__ +#define _EA_FACTORY_H__ + +#include <atk/atkobject.h> + +#define EA_FACTORY_PARTA_GOBJECT(type, type_as_function, opt_create_accessible) \ +static AtkObject * \ +type_as_function ## _factory_create_accessible (GObject *obj) \ +{ \ + AtkObject *accessible; \ + g_return_val_if_fail (G_IS_OBJECT (obj), NULL); \ + accessible = opt_create_accessible (G_OBJECT (obj)); \ + return accessible; \ +} + +#define EA_FACTORY_PARTA(type, type_as_function, opt_create_accessible) \ +static AtkObject * \ +type_as_function ## _factory_create_accessible (GObject *obj) \ +{ \ + GtkWidget *widget; \ + AtkObject *accessible; \ + \ + g_return_val_if_fail (GTK_IS_WIDGET (obj), NULL); \ + \ + widget = GTK_WIDGET (obj); \ + \ + accessible = opt_create_accessible (widget); \ + return accessible; \ +} + +#define EA_FACTORY_PARTB(type, type_as_function, opt_create_accessible) \ + \ +static GType \ +type_as_function ## _factory_get_accessible_type (void) \ +{ \ + return type; \ +} \ + \ + \ +static void \ +type_as_function ## _factory_class_init (AtkObjectFactoryClass *klass) \ +{ \ + klass->create_accessible = type_as_function ## _factory_create_accessible; \ + klass->get_accessible_type = type_as_function ## _factory_get_accessible_type;\ +} \ + \ +static GType \ +type_as_function ## _factory_get_type (void) \ +{ \ + static GType t = 0; \ + \ + if (!t) \ + { \ + gchar *name; \ + static const GTypeInfo tinfo = \ + { \ + sizeof (AtkObjectFactoryClass), \ + NULL, NULL, (GClassInitFunc) type_as_function ## _factory_class_init, \ + NULL, NULL, sizeof (AtkObjectFactory), 0, NULL, NULL \ + }; \ + \ + name = g_strconcat (g_type_name (type), "Factory", NULL); \ + t = g_type_register_static ( \ + ATK_TYPE_OBJECT_FACTORY, name, &tinfo, 0); \ + g_free (name); \ + } \ + \ + return t; \ +} + +#define EA_FACTORY(type, type_as_function, opt_create_accessible) \ + EA_FACTORY_PARTA (type, type_as_function, opt_create_accessible) \ + EA_FACTORY_PARTB (type, type_as_function, opt_create_accessible) + +#define EA_FACTORY_GOBJECT(type, type_as_function, opt_create_accessible) \ + EA_FACTORY_PARTA_GOBJECT (type, type_as_function, opt_create_accessible) \ + EA_FACTORY_PARTB (type, type_as_function, opt_create_accessible) + +#define EA_SET_FACTORY(obj_type, type_as_function) \ +{ \ + if (atk_get_root ()) { \ + atk_registry_set_factory_type (atk_get_default_registry (), \ + obj_type, \ + type_as_function ## _factory_get_type ());\ + } \ +} + +#endif /* _EA_FACTORY_H__ */ diff --git a/e-util/ea-widgets.c b/e-util/ea-widgets.c new file mode 100644 index 0000000000..0a65730359 --- /dev/null +++ b/e-util/ea-widgets.c @@ -0,0 +1,36 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "ea-factory.h" +#include "ea-calendar-item.h" +#include "ea-widgets.h" + +EA_FACTORY_GOBJECT (EA_TYPE_CALENDAR_ITEM, ea_calendar_item, ea_calendar_item_new) + +void e_calendar_item_a11y_init (void) +{ + EA_SET_FACTORY (e_calendar_item_get_type (), ea_calendar_item); +} diff --git a/e-util/ea-widgets.h b/e-util/ea-widgets.h new file mode 100644 index 0000000000..3fd212ff94 --- /dev/null +++ b/e-util/ea-widgets.h @@ -0,0 +1,36 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +/* Evolution Accessibility +*/ + +#ifndef _EA_WIDGETS_H__ +#define _EA_WIDGETS_H__ + +void e_calendar_item_a11y_init (void); + +#endif /* _EA_WIDGETS_H__ */ diff --git a/e-util/evolution-source-viewer.c b/e-util/evolution-source-viewer.c new file mode 100644 index 0000000000..9f5fb117a5 --- /dev/null +++ b/e-util/evolution-source-viewer.c @@ -0,0 +1,1176 @@ +/* + * evolution-source-viewer.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include <config.h> +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include <libedataserver/libedataserver.h> + +/* XXX Even though this is all one file, I'm still being pedantic about data + * encapsulation (except for a private struct, even I'm not that anal!). + * I expect this program will eventually be too complex for one file + * and we'll want to split off an e-source-viewer.[ch]. */ + +/* Standard GObject macros */ +#define E_TYPE_SOURCE_VIEWER \ + (e_source_viewer_get_type ()) +#define E_SOURCE_VIEWER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SOURCE_VIEWER, ESourceViewer)) +#define E_SOURCE_VIEWER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SOURCE_VIEWER, ESourceViewerClass)) +#define E_IS_SOURCE_VIEWER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SOURCE_VIEWER)) +#define E_IS_SOURCE_VIEWER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SOURCE_VIEWER)) +#define E_SOURCE_VIEWER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SOURCE_VIEWER, ESourceViewerClass)) + +typedef struct _ESourceViewer ESourceViewer; +typedef struct _ESourceViewerClass ESourceViewerClass; + +struct _ESourceViewer { + GtkWindow parent; + ESourceRegistry *registry; + + GtkTreeStore *tree_store; + GHashTable *source_index; + + GCancellable *delete_operation; + + GtkWidget *tree_view; /* not referenced */ + GtkWidget *text_view; /* not referenced */ + GtkWidget *top_panel; /* not referenced */ + + /* Viewing Page */ + GtkWidget *viewing_label; /* not referenced */ + GtkWidget *delete_button; /* not referenced */ + + /* Deleting Page */ + GtkWidget *deleting_label; /* not referenced */ + GtkWidget *deleting_cancel; /* not referenced */ +}; + +struct _ESourceViewerClass { + GtkWindowClass parent_class; +}; + +enum { + PAGE_VIEWING, + PAGE_DELETING +}; + +enum { + PROP_0, + PROP_REGISTRY +}; + +enum { + COLUMN_DISPLAY_NAME, + COLUMN_SOURCE_UID, + COLUMN_REMOVABLE, + COLUMN_WRITABLE, + COLUMN_REMOTE_CREATABLE, + COLUMN_REMOTE_DELETABLE, + COLUMN_SOURCE, + NUM_COLUMNS +}; + +/* Forward Declarations */ +GType e_source_viewer_get_type (void) G_GNUC_CONST; +GtkWidget * e_source_viewer_new (GCancellable *cancellable, + GError **error); +ESourceRegistry * + e_source_viewer_get_registry (ESourceViewer *viewer); +GtkTreePath * e_source_viewer_dup_selected_path + (ESourceViewer *viewer); +gboolean e_source_viewer_set_selected_path + (ESourceViewer *viewer, + GtkTreePath *path); +ESource * e_source_viewer_ref_selected_source + (ESourceViewer *viewer); +gboolean e_source_viewer_set_selected_source + (ESourceViewer *viewer, + ESource *source); +GNode * e_source_viewer_build_display_tree + (ESourceViewer *viewer); + +static void e_source_viewer_initable_init (GInitableIface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + ESourceViewer, + e_source_viewer, + GTK_TYPE_WINDOW, + G_IMPLEMENT_INTERFACE ( + G_TYPE_INITABLE, + e_source_viewer_initable_init)); + +static GIcon * +source_view_new_remote_creatable_icon (void) +{ + GEmblem *emblem; + GIcon *emblem_icon; + GIcon *folder_icon; + GIcon *icon; + + emblem_icon = g_themed_icon_new ("emblem-new"); + folder_icon = g_themed_icon_new ("folder-remote"); + + emblem = g_emblem_new (emblem_icon); + icon = g_emblemed_icon_new (folder_icon, emblem); + g_object_unref (emblem); + + g_object_unref (folder_icon); + g_object_unref (emblem_icon); + + return icon; +} + +static GIcon * +source_view_new_remote_deletable_icon (void) +{ + GEmblem *emblem; + GIcon *emblem_icon; + GIcon *folder_icon; + GIcon *icon; + + emblem_icon = g_themed_icon_new ("edit-delete"); + folder_icon = g_themed_icon_new ("folder-remote"); + + emblem = g_emblem_new (emblem_icon); + icon = g_emblemed_icon_new (folder_icon, emblem); + g_object_unref (emblem); + + g_object_unref (folder_icon); + g_object_unref (emblem_icon); + + return icon; +} + +static gchar * +source_viewer_get_monospace_font_name (void) +{ + GSettings *settings; + gchar *font_name; + + settings = g_settings_new ("org.gnome.desktop.interface"); + font_name = g_settings_get_string (settings, "monospace-font-name"); + g_object_unref (settings); + + /* Fallback to a reasonable default. */ + if (font_name == NULL) + font_name = g_strdup ("Monospace 10"); + + return font_name; +} + +static void +source_viewer_set_text (ESourceViewer *viewer, + ESource *source) +{ + GtkTextView *text_view; + GtkTextBuffer *buffer; + GtkTextIter start; + GtkTextIter end; + + text_view = GTK_TEXT_VIEW (viewer->text_view); + buffer = gtk_text_view_get_buffer (text_view); + + gtk_text_buffer_get_start_iter (buffer, &start); + gtk_text_buffer_get_end_iter (buffer, &end); + gtk_text_buffer_delete (buffer, &start, &end); + + if (source != NULL) { + gchar *string; + gsize length; + + gtk_text_buffer_get_start_iter (buffer, &start); + + string = e_source_to_string (source, &length); + gtk_text_buffer_insert (buffer, &start, string, length); + g_free (string); + } +} + +static void +source_viewer_update_row (ESourceViewer *viewer, + ESource *source) +{ + GHashTable *source_index; + GtkTreeRowReference *reference; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + const gchar *display_name; + const gchar *source_uid; + gboolean removable; + gboolean writable; + gboolean remote_creatable; + gboolean remote_deletable; + + source_index = viewer->source_index; + reference = g_hash_table_lookup (source_index, source); + + /* We show all sources, so the reference should be valid. */ + g_return_if_fail (gtk_tree_row_reference_valid (reference)); + + model = gtk_tree_row_reference_get_model (reference); + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + + source_uid = e_source_get_uid (source); + display_name = e_source_get_display_name (source); + removable = e_source_get_removable (source); + writable = e_source_get_writable (source); + remote_creatable = e_source_get_remote_creatable (source); + remote_deletable = e_source_get_remote_deletable (source); + + gtk_tree_store_set ( + GTK_TREE_STORE (model), &iter, + COLUMN_DISPLAY_NAME, display_name, + COLUMN_SOURCE_UID, source_uid, + COLUMN_REMOVABLE, removable, + COLUMN_WRITABLE, writable, + COLUMN_REMOTE_CREATABLE, remote_creatable, + COLUMN_REMOTE_DELETABLE, remote_deletable, + COLUMN_SOURCE, source, + -1); +} + +static gboolean +source_viewer_traverse (GNode *node, + gpointer user_data) +{ + ESourceViewer *viewer; + ESource *source; + GHashTable *source_index; + GtkTreeRowReference *reference = NULL; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + + /* Skip the root node. */ + if (G_NODE_IS_ROOT (node)) + return FALSE; + + viewer = E_SOURCE_VIEWER (user_data); + + source_index = viewer->source_index; + + tree_view = GTK_TREE_VIEW (viewer->tree_view); + model = gtk_tree_view_get_model (tree_view); + + if (node->parent != NULL && node->parent->data != NULL) + reference = g_hash_table_lookup ( + source_index, node->parent->data); + + if (gtk_tree_row_reference_valid (reference)) { + GtkTreeIter parent; + + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_model_get_iter (model, &parent, path); + gtk_tree_path_free (path); + + gtk_tree_store_append (GTK_TREE_STORE (model), &iter, &parent); + } else + gtk_tree_store_append (GTK_TREE_STORE (model), &iter, NULL); + + /* Source index takes ownership. */ + source = g_object_ref (node->data); + + path = gtk_tree_model_get_path (model, &iter); + reference = gtk_tree_row_reference_new (model, path); + g_hash_table_insert (source_index, source, reference); + gtk_tree_path_free (path); + + source_viewer_update_row (viewer, source); + + return FALSE; +} + +static void +source_viewer_save_expanded (GtkTreeView *tree_view, + GtkTreePath *path, + GQueue *queue) +{ + GtkTreeModel *model; + GtkTreeIter iter; + ESource *source; + + model = gtk_tree_view_get_model (tree_view); + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + g_queue_push_tail (queue, source); +} + +static void +source_viewer_build_model (ESourceViewer *viewer) +{ + GQueue queue = G_QUEUE_INIT; + GHashTable *source_index; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreePath *sel_path; + ESource *sel_source; + GNode *root; + + tree_view = GTK_TREE_VIEW (viewer->tree_view); + + source_index = viewer->source_index; + sel_path = e_source_viewer_dup_selected_path (viewer); + sel_source = e_source_viewer_ref_selected_source (viewer); + + /* Save expanded sources to restore later. */ + gtk_tree_view_map_expanded_rows ( + tree_view, (GtkTreeViewMappingFunc) + source_viewer_save_expanded, &queue); + + model = gtk_tree_view_get_model (tree_view); + gtk_tree_store_clear (GTK_TREE_STORE (model)); + + g_hash_table_remove_all (source_index); + + root = e_source_viewer_build_display_tree (viewer); + + g_node_traverse ( + root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + (GNodeTraverseFunc) source_viewer_traverse, viewer); + + e_source_registry_free_display_tree (root); + + /* Restore previously expanded sources. */ + while (!g_queue_is_empty (&queue)) { + GtkTreeRowReference *reference; + ESource *source; + + source = g_queue_pop_head (&queue); + reference = g_hash_table_lookup (source_index, source); + + if (gtk_tree_row_reference_valid (reference)) { + GtkTreePath *path; + + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_view_expand_to_path (tree_view, path); + gtk_tree_path_free (path); + } + + g_object_unref (source); + } + + /* Restore the selection. */ + if (sel_source != NULL && sel_path != NULL) { + if (!e_source_viewer_set_selected_source (viewer, sel_source)) + e_source_viewer_set_selected_path (viewer, sel_path); + } + + if (sel_path != NULL) + gtk_tree_path_free (sel_path); + + if (sel_source != NULL) + g_object_unref (sel_source); +} + +static void +source_viewer_expand_to_source (ESourceViewer *viewer, + ESource *source) +{ + GHashTable *source_index; + GtkTreeRowReference *reference; + GtkTreeView *tree_view; + GtkTreePath *path; + + source_index = viewer->source_index; + reference = g_hash_table_lookup (source_index, source); + + /* We show all sources, so the reference should be valid. */ + g_return_if_fail (gtk_tree_row_reference_valid (reference)); + + /* Expand the tree view to the path containing the ESource. */ + tree_view = GTK_TREE_VIEW (viewer->tree_view); + path = gtk_tree_row_reference_get_path (reference); + gtk_tree_view_expand_to_path (tree_view, path); + gtk_tree_path_free (path); +} + +static void +source_viewer_source_added_cb (ESourceRegistry *registry, + ESource *source, + ESourceViewer *viewer) +{ + source_viewer_build_model (viewer); + + source_viewer_expand_to_source (viewer, source); +} + +static void +source_viewer_source_changed_cb (ESourceRegistry *registry, + ESource *source, + ESourceViewer *viewer) +{ + ESource *selected; + + source_viewer_update_row (viewer, source); + + selected = e_source_viewer_ref_selected_source (viewer); + if (selected != NULL) { + if (e_source_equal (source, selected)) + source_viewer_set_text (viewer, source); + g_object_unref (selected); + } +} + +static void +source_viewer_source_removed_cb (ESourceRegistry *registry, + ESource *source, + ESourceViewer *viewer) +{ + source_viewer_build_model (viewer); +} + +static void +source_viewer_selection_changed_cb (GtkTreeSelection *selection, + ESourceViewer *viewer) +{ + ESource *source; + const gchar *uid = NULL; + gboolean removable = FALSE; + + source = e_source_viewer_ref_selected_source (viewer); + + source_viewer_set_text (viewer, source); + + if (source != NULL) { + uid = e_source_get_uid (source); + removable = e_source_get_removable (source); + } + + gtk_label_set_text (GTK_LABEL (viewer->viewing_label), uid); + gtk_widget_set_visible (viewer->delete_button, removable); + + if (source != NULL) + g_object_unref (source); +} + +static void +source_viewer_delete_done_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + ESource *source; + ESourceViewer *viewer; + GError *error = NULL; + + source = E_SOURCE (source_object); + viewer = E_SOURCE_VIEWER (user_data); + + e_source_remove_finish (source, result, &error); + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_clear_error (&error); + + /* FIXME Show an info bar with the error message. */ + } else if (error != NULL) { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_clear_error (&error); + } + + gtk_notebook_set_current_page ( + GTK_NOTEBOOK (viewer->top_panel), PAGE_VIEWING); + gtk_widget_set_sensitive (viewer->tree_view, TRUE); + + g_object_unref (viewer->delete_operation); + viewer->delete_operation = NULL; + + g_object_unref (viewer); +} + +static void +source_viewer_delete_button_clicked_cb (GtkButton *delete_button, + ESourceViewer *viewer) +{ + ESource *source; + const gchar *uid; + + g_return_if_fail (viewer->delete_operation == NULL); + + source = e_source_viewer_ref_selected_source (viewer); + g_return_if_fail (source != NULL); + + uid = e_source_get_uid (source); + gtk_label_set_text (GTK_LABEL (viewer->deleting_label), uid); + + gtk_notebook_set_current_page ( + GTK_NOTEBOOK (viewer->top_panel), PAGE_DELETING); + gtk_widget_set_sensitive (viewer->tree_view, FALSE); + + viewer->delete_operation = g_cancellable_new (); + + e_source_remove ( + source, + viewer->delete_operation, + source_viewer_delete_done_cb, + g_object_ref (viewer)); + + g_object_unref (source); +} + +static void +source_viewer_deleting_cancel_clicked_cb (GtkButton *deleting_cancel, + ESourceViewer *viewer) +{ + g_return_if_fail (viewer->delete_operation != NULL); + + g_cancellable_cancel (viewer->delete_operation); +} + +static void +source_viewer_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_REGISTRY: + g_value_set_object ( + value, + e_source_viewer_get_registry ( + E_SOURCE_VIEWER (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +source_viewer_dispose (GObject *object) +{ + ESourceViewer *viewer = E_SOURCE_VIEWER (object); + + if (viewer->registry != NULL) { + g_signal_handlers_disconnect_matched ( + viewer->registry, + G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (viewer->registry); + viewer->registry = NULL; + } + + if (viewer->tree_store != NULL) { + g_object_unref (viewer->tree_store); + viewer->tree_store = NULL; + } + + g_hash_table_remove_all (viewer->source_index); + + if (viewer->delete_operation != NULL) { + g_object_unref (viewer->delete_operation); + viewer->delete_operation = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_source_viewer_parent_class)->dispose (object); +} + +static void +source_viewer_finalize (GObject *object) +{ + ESourceViewer *viewer = E_SOURCE_VIEWER (object); + + g_hash_table_destroy (viewer->source_index); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_source_viewer_parent_class)->finalize (object); +} + +static void +source_viewer_constructed (GObject *object) +{ + ESourceViewer *viewer; + GtkTreeViewColumn *column; + GtkTreeSelection *selection; + GtkCellRenderer *renderer; + GtkWidget *container; + GtkWidget *paned; + GtkWidget *widget; + PangoAttribute *attr; + PangoAttrList *bold; + PangoFontDescription *desc; + GIcon *icon; + const gchar *title; + gchar *font_name; + gint page_num; + + viewer = E_SOURCE_VIEWER (object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_source_viewer_parent_class)->constructed (object); + + bold = pango_attr_list_new (); + attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD); + pango_attr_list_insert (bold, attr); + + title = _("Evolution Source Viewer"); + gtk_window_set_title (GTK_WINDOW (viewer), title); + gtk_window_set_default_size (GTK_WINDOW (viewer), 800, 600); + + paned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL); + gtk_paned_set_position (GTK_PANED (paned), 400); + gtk_container_add (GTK_CONTAINER (viewer), paned); + gtk_widget_show (paned); + + /* Left panel */ + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_paned_add1 (GTK_PANED (paned), widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_tree_view_new_with_model ( + GTK_TREE_MODEL (viewer->tree_store)); + gtk_container_add (GTK_CONTAINER (container), widget); + viewer->tree_view = widget; /* do not reference */ + gtk_widget_show (widget); + + column = gtk_tree_view_column_new (); + /* Translators: The name that is displayed in the user interface */ + gtk_tree_view_column_set_title (column, _("Display Name")); + gtk_tree_view_append_column (GTK_TREE_VIEW (widget), column); + + renderer = gtk_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + gtk_tree_view_column_add_attribute ( + column, renderer, "text", COLUMN_DISPLAY_NAME); + + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (column, _("Flags")); + gtk_tree_view_append_column (GTK_TREE_VIEW (widget), column); + + renderer = gtk_cell_renderer_pixbuf_new (); + g_object_set ( + renderer, + "stock-id", GTK_STOCK_EDIT, + "stock-size", GTK_ICON_SIZE_MENU, + NULL); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + gtk_tree_view_column_add_attribute ( + column, renderer, "visible", COLUMN_WRITABLE); + + renderer = gtk_cell_renderer_pixbuf_new (); + g_object_set ( + renderer, + "stock-id", GTK_STOCK_DELETE, + "stock-size", GTK_ICON_SIZE_MENU, + NULL); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + gtk_tree_view_column_add_attribute ( + column, renderer, "visible", COLUMN_REMOVABLE); + + icon = source_view_new_remote_creatable_icon (); + renderer = gtk_cell_renderer_pixbuf_new (); + g_object_set ( + renderer, + "gicon", icon, + "stock-size", GTK_ICON_SIZE_MENU, + NULL); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + gtk_tree_view_column_add_attribute ( + column, renderer, "visible", COLUMN_REMOTE_CREATABLE); + g_object_unref (icon); + + icon = source_view_new_remote_deletable_icon (); + renderer = gtk_cell_renderer_pixbuf_new (); + g_object_set ( + renderer, + "gicon", icon, + "stock-size", GTK_ICON_SIZE_MENU, + NULL); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + gtk_tree_view_column_add_attribute ( + column, renderer, "visible", COLUMN_REMOTE_DELETABLE); + g_object_unref (icon); + + /* Append an empty pixbuf renderer to fill leftover space. */ + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (column, _("Identity")); + gtk_tree_view_append_column (GTK_TREE_VIEW (widget), column); + + renderer = gtk_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + gtk_tree_view_column_add_attribute ( + column, renderer, "text", COLUMN_SOURCE_UID); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget)); + + /* Right panel */ + + widget = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_paned_add2 (GTK_PANED (paned), widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_notebook_new (); + gtk_widget_set_margin_top (widget, 3); + gtk_widget_set_margin_right (widget, 3); + gtk_widget_set_margin_bottom (widget, 3); + /* leave left margin at zero */ + gtk_notebook_set_show_tabs (GTK_NOTEBOOK (widget), FALSE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + viewer->top_panel = widget; /* do not reference */ + gtk_widget_show (widget); + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_text_view_new (); + gtk_text_view_set_editable (GTK_TEXT_VIEW (widget), FALSE); + gtk_container_add (GTK_CONTAINER (container), widget); + viewer->text_view = widget; /* do not reference */ + gtk_widget_show (widget); + + font_name = source_viewer_get_monospace_font_name (); + desc = pango_font_description_from_string (font_name); + gtk_widget_override_font (widget, desc); + pango_font_description_free (desc); + g_free (font_name); + + /* Top panel: Viewing */ + + container = viewer->top_panel; + + widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + page_num = gtk_notebook_append_page ( + GTK_NOTEBOOK (container), widget, NULL); + g_warn_if_fail (page_num == PAGE_VIEWING); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_label_new ("Identity:"); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_label_new (NULL); + gtk_label_set_attributes (GTK_LABEL (widget), bold); + gtk_label_set_ellipsize (GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + viewer->viewing_label = widget; /* do not reference */ + gtk_widget_show (widget); + + widget = gtk_button_new_from_stock (GTK_STOCK_DELETE); + gtk_box_pack_end (GTK_BOX (container), widget, FALSE, FALSE, 0); + viewer->delete_button = widget; /* do not reference */ + gtk_widget_hide (widget); + + /* Top panel: Deleting */ + + container = viewer->top_panel; + + widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + page_num = gtk_notebook_append_page ( + GTK_NOTEBOOK (container), widget, NULL); + g_warn_if_fail (page_num == PAGE_DELETING); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_label_new ("Deleting"); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_label_new (NULL); + gtk_label_set_attributes (GTK_LABEL (widget), bold); + gtk_label_set_ellipsize (GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + viewer->deleting_label = widget; /* do not reference */ + gtk_widget_show (widget); + + widget = gtk_button_new_from_stock (GTK_STOCK_CANCEL); + gtk_box_pack_end (GTK_BOX (container), widget, FALSE, FALSE, 0); + viewer->deleting_cancel = widget; /* do not reference */ + gtk_widget_show (widget); + + pango_attr_list_unref (bold); + + g_signal_connect ( + selection, "changed", + G_CALLBACK (source_viewer_selection_changed_cb), viewer); + + g_signal_connect ( + viewer->delete_button, "clicked", + G_CALLBACK (source_viewer_delete_button_clicked_cb), viewer); + + g_signal_connect ( + viewer->deleting_cancel, "clicked", + G_CALLBACK (source_viewer_deleting_cancel_clicked_cb), viewer); +} + +static gboolean +source_viewer_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + ESourceViewer *viewer; + ESourceRegistry *registry; + + viewer = E_SOURCE_VIEWER (initable); + + registry = e_source_registry_new_sync (cancellable, error); + + if (registry == NULL) + return FALSE; + + viewer->registry = registry; /* takes ownership */ + + g_signal_connect ( + registry, "source-added", + G_CALLBACK (source_viewer_source_added_cb), viewer); + + g_signal_connect ( + registry, "source-changed", + G_CALLBACK (source_viewer_source_changed_cb), viewer); + + g_signal_connect ( + registry, "source-removed", + G_CALLBACK (source_viewer_source_removed_cb), viewer); + + source_viewer_build_model (viewer); + + gtk_tree_view_expand_all (GTK_TREE_VIEW (viewer->tree_view)); + + return TRUE; +} + +static void +e_source_viewer_class_init (ESourceViewerClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = source_viewer_get_property; + object_class->dispose = source_viewer_dispose; + object_class->finalize = source_viewer_finalize; + object_class->constructed = source_viewer_constructed; + + g_object_class_install_property ( + object_class, + PROP_REGISTRY, + g_param_spec_object ( + "registry", + "Registry", + "Data source registry", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_source_viewer_initable_init (GInitableIface *interface) +{ + interface->init = source_viewer_initable_init; +} + +static void +e_source_viewer_init (ESourceViewer *viewer) +{ + viewer->tree_store = gtk_tree_store_new ( + NUM_COLUMNS, + G_TYPE_STRING, /* COLUMN_DISPLAY_NAME */ + G_TYPE_STRING, /* COLUMN_SOURCE_UID */ + G_TYPE_BOOLEAN, /* COLUMN_REMOVABLE */ + G_TYPE_BOOLEAN, /* COLUMN_WRITABLE */ + G_TYPE_BOOLEAN, /* COLUMN_REMOTE_CREATABLE */ + G_TYPE_BOOLEAN, /* COLUMN_REMOTE_DELETABLE */ + E_TYPE_SOURCE); /* COLUMN_SOURCE */ + + viewer->source_index = g_hash_table_new_full ( + (GHashFunc) e_source_hash, + (GEqualFunc) e_source_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) gtk_tree_row_reference_free); +} + +GtkWidget * +e_source_viewer_new (GCancellable *cancellable, + GError **error) +{ + return g_initable_new ( + E_TYPE_SOURCE_VIEWER, + cancellable, error, NULL); +} + +ESourceRegistry * +e_source_viewer_get_registry (ESourceViewer *viewer) +{ + g_return_val_if_fail (E_IS_SOURCE_VIEWER (viewer), NULL); + + return viewer->registry; +} + +GtkTreePath * +e_source_viewer_dup_selected_path (ESourceViewer *viewer) +{ + GtkTreeSelection *selection; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreeIter iter; + + g_return_val_if_fail (E_IS_SOURCE_VIEWER (viewer), NULL); + + tree_view = GTK_TREE_VIEW (viewer->tree_view); + selection = gtk_tree_view_get_selection (tree_view); + + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) + return NULL; + + return gtk_tree_model_get_path (model, &iter); +} + +gboolean +e_source_viewer_set_selected_path (ESourceViewer *viewer, + GtkTreePath *path) +{ + GtkTreeSelection *selection; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreeIter iter; + + g_return_val_if_fail (E_IS_SOURCE_VIEWER (viewer), FALSE); + g_return_val_if_fail (path != NULL, FALSE); + + tree_view = GTK_TREE_VIEW (viewer->tree_view); + selection = gtk_tree_view_get_selection (tree_view); + + /* Check that the path is valid. */ + model = gtk_tree_view_get_model (tree_view); + if (!gtk_tree_model_get_iter (model, &iter, path)) + return FALSE; + + gtk_tree_selection_unselect_all (selection); + + gtk_tree_view_expand_to_path (tree_view, path); + gtk_tree_selection_select_path (selection, path); + + return TRUE; +} + +ESource * +e_source_viewer_ref_selected_source (ESourceViewer *viewer) +{ + ESource *source; + GtkTreeSelection *selection; + GtkTreeView *tree_view; + GtkTreeModel *model; + GtkTreeIter iter; + + g_return_val_if_fail (E_IS_SOURCE_VIEWER (viewer), NULL); + + tree_view = GTK_TREE_VIEW (viewer->tree_view); + selection = gtk_tree_view_get_selection (tree_view); + + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) + return NULL; + + gtk_tree_model_get (model, &iter, COLUMN_SOURCE, &source, -1); + + return source; +} + +gboolean +e_source_viewer_set_selected_source (ESourceViewer *viewer, + ESource *source) +{ + GHashTable *source_index; + GtkTreeRowReference *reference; + GtkTreePath *path; + gboolean success; + + g_return_val_if_fail (E_IS_SOURCE_VIEWER (viewer), FALSE); + g_return_val_if_fail (E_IS_SOURCE (source), FALSE); + + source_index = viewer->source_index; + reference = g_hash_table_lookup (source_index, source); + + if (!gtk_tree_row_reference_valid (reference)) + return FALSE; + + path = gtk_tree_row_reference_get_path (reference); + success = e_source_viewer_set_selected_path (viewer, path); + gtk_tree_path_free (path); + + return success; +} + +/* Helper for e_source_viewer_build_display_tree() */ +static gint +source_viewer_compare_nodes (GNode *node_a, + GNode *node_b) +{ + ESource *source_a = E_SOURCE (node_a->data); + ESource *source_b = E_SOURCE (node_b->data); + + return e_source_compare_by_display_name (source_a, source_b); +} + +/* Helper for e_source_viewer_build_display_tree() */ +static gboolean +source_viewer_sort_nodes (GNode *node, + gpointer unused) +{ + GQueue queue = G_QUEUE_INIT; + GNode *child_node; + + /* Unlink all the child nodes and place them in a queue. */ + while ((child_node = g_node_first_child (node)) != NULL) { + g_node_unlink (child_node); + g_queue_push_tail (&queue, child_node); + } + + /* Sort the queue by source name. */ + g_queue_sort ( + &queue, (GCompareDataFunc) + source_viewer_compare_nodes, NULL); + + /* Pop nodes off the head of the queue and put them back + * under the parent node (preserving the sorted order). */ + while ((child_node = g_queue_pop_head (&queue)) != NULL) + g_node_append (node, child_node); + + return FALSE; +} + +GNode * +e_source_viewer_build_display_tree (ESourceViewer *viewer) +{ + GNode *root; + GHashTable *index; + GList *list, *link; + GHashTableIter iter; + gpointer value; + + /* This is just like e_source_registry_build_display_tree() + * except it includes all data sources, even disabled ones. + * Free the tree with e_source_registry_free_display_tree(). */ + + g_return_val_if_fail (E_IS_SOURCE_VIEWER (viewer), NULL); + + root = g_node_new (NULL); + index = g_hash_table_new (g_str_hash, g_str_equal); + + /* Add a GNode for each ESource to the index. + * The GNodes take ownership of the ESource references. */ + list = e_source_registry_list_sources (viewer->registry, NULL); + for (link = list; link != NULL; link = g_list_next (link)) { + ESource *source = E_SOURCE (link->data); + gpointer key = (gpointer) e_source_get_uid (source); + g_hash_table_insert (index, key, g_node_new (source)); + } + g_list_free (list); + + /* Traverse the index and link the nodes together. */ + g_hash_table_iter_init (&iter, index); + while (g_hash_table_iter_next (&iter, NULL, &value)) { + ESource *source; + GNode *source_node; + GNode *parent_node; + const gchar *parent_uid; + + source_node = (GNode *) value; + source = E_SOURCE (source_node->data); + parent_uid = e_source_get_parent (source); + + if (parent_uid == NULL || *parent_uid == '\0') { + parent_node = root; + } else { + parent_node = g_hash_table_lookup (index, parent_uid); + } + + /* This could be NULL if the registry service was + * shutdown or reloaded. All sources will vanish. */ + if (parent_node != NULL) + g_node_append (parent_node, source_node); + } + + /* Sort nodes by display name in post order. */ + g_node_traverse ( + root, G_POST_ORDER, G_TRAVERSE_ALL, + -1, source_viewer_sort_nodes, NULL); + + g_hash_table_destroy (index); + + return root; +} + +gint +main (gint argc, + gchar **argv) +{ + GtkWidget *viewer; + GError *error = NULL; + + bindtextdomain (GETTEXT_PACKAGE, EVOLUTION_LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + gtk_init (&argc, &argv); + + viewer = e_source_viewer_new (NULL, &error); + + if (error != NULL) { + g_warn_if_fail (viewer == NULL); + g_error ("%s", error->message); + g_assert_not_reached (); + } + + g_signal_connect ( + viewer, "delete-event", + G_CALLBACK (gtk_main_quit), NULL); + + gtk_widget_show (viewer); + + gtk_main (); + + return 0; +} diff --git a/e-util/filter.error.xml b/e-util/filter.error.xml new file mode 100644 index 0000000000..62b75193d2 --- /dev/null +++ b/e-util/filter.error.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<error-list domain="filter"> + + <error id="no-date" type="error"> + <_primary>Missing date.</_primary> + <_secondary>You must choose a date.</_secondary> + </error> + + <error id="no-file" type="error"> + <_primary>Missing filename.</_primary> + <_secondary>You must specify a filename.</_secondary> + </error> + + <error id="bad-file" type="error"> + <_primary>File "{0}" does not exist or is not a regular file.</_primary> + <_secondary>You must specify a filename.</_secondary> + </error> + + <error id="bad-regexp" type="error"> + <_primary>Bad regular expression "{0}".</_primary> + <_secondary>Could not compile regular expression "{1}".</_secondary> + </error> + + <error id="no-name" type="error"> + <_primary>Missing name.</_primary> + <_secondary>You must name this filter.</_secondary> + </error> + + <error id="bad-name-notunique" type="error"> + <_primary>Name "{0}" already used.</_primary> + <_secondary>Please choose another name.</_secondary> + </error> + +</error-list> diff --git a/e-util/filter.ui b/e-util/filter.ui new file mode 100644 index 0000000000..d91292736d --- /dev/null +++ b/e-util/filter.ui @@ -0,0 +1,591 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkAdjustment" id="adjustment1"> + <property name="value">1</property> + <property name="upper">1000</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkListStore" id="model1"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">Incoming</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model2"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">the current time</col> + </row> + <row> + <col id="0" translatable="yes">the time you specify</col> + </row> + <row> + <col id="0" translatable="yes">a time relative to the current time</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model3"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">seconds</col> + </row> + <row> + <col id="0" translatable="yes">minutes</col> + </row> + <row> + <col id="0" translatable="yes">hours</col> + </row> + <row> + <col id="0" translatable="yes">days</col> + </row> + <row> + <col id="0" translatable="yes">weeks</col> + </row> + <row> + <col id="0" translatable="yes">months</col> + </row> + <row> + <col id="0" translatable="yes">years</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model4"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">ago</col> + </row> + <row> + <col id="0" translatable="yes">in the future</col> + </row> + </data> + </object> + <object class="GtkListStore" id="rule_list_store"> + <columns> + <!-- column-name column1 --> + <column type="gchararray"/> + <!-- column-name column2 --> + <column type="gpointer"/> + <!-- column-name column3 --> + <column type="gboolean"/> + </columns> + </object> + <object class="GtkVBox" id="rule_editor"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label17"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Show filters for mail:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="filter_source_combobox"> + <property name="visible">True</property> + <property name="model">model1</property> + <child> + <object class="GtkCellRendererText" id="renderer1"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="rule_frame"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="rule_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Filter Rules</property> + <property name="use_underline">True</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox10"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="label16"> + <property name="visible">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox4"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkScrolledWindow" id="rule_scrolled_window"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="rule_tree_view"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">rule_list_store</property> + <property name="headers_visible">False</property> + <child> + <object class="GtkTreeViewColumn" id="column_enabled"> + <!--<property name="visible">False</property>--> + <property name="title">Enabled</property> + <child> + <object class="GtkCellRendererToggle" id="cell_renderer_enabled"/> + <attributes> + <attribute name="active">2</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="column_rule_name"> + <property name="title">Rule Name</property> + <child> + <object class="GtkCellRendererText" id="cell_renderer_rule_name"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox5"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkVButtonBox" id="vbuttonbox4"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkButton" id="rule_add"> + <property name="label">gtk-add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="rule_edit"> + <property name="label" translatable="yes">_Edit</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="rule_delete"> + <property name="label">gtk-remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="rule_top"> + <property name="label">gtk-goto-top</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkButton" id="rule_up"> + <property name="label">gtk-go-up</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkButton" id="rule_down"> + <property name="label">gtk-go-down</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkButton" id="rule_bottom"> + <property name="label">gtk-goto-bottom</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">6</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="padding">3</property> + <property name="position">2</property> + </packing> + </child> + </object> + <object class="GtkVBox" id="filter_datespec"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkHBox" id="hbox5"> + <property name="visible">True</property> + <property name="border_width">4</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="label" translatable="yes">Compare against</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="combobox_type"> + <property name="visible">True</property> + <property name="model">model2</property> + <child> + <object class="GtkCellRendererText" id="renderer2"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator1"> + <property name="visible">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="padding">1</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkNotebook" id="notebook_type"> + <property name="visible">True</property> + <property name="show_tabs">False</property> + <property name="show_border">False</property> + <child> + <object class="GtkVBox" id="vbox9"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="label" translatable="yes">The message's date will be compared against +the current time when filtering occurs.</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="label" translatable="yes">label1</property> + <property name="justify">center</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox7"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="label" translatable="yes">The message's date will be compared against +12:00am of the date specified.</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCalendar" id="calendar_specify"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="label" translatable="yes">label2</property> + <property name="justify">center</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox8"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="ypad">15</property> + <property name="label" translatable="yes">The message's date will be compared against +a time relative to when filtering occurs.</property> + <property name="justify">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="top_padding">5</property> + <property name="left_padding">58</property> + <child> + <object class="GtkHBox" id="hbox6"> + <property name="visible">True</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkSpinButton" id="spin_relative"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment1</property> + <property name="climb_rate">1</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="combobox_relative"> + <property name="visible">True</property> + <property name="model">model3</property> + <property name="active">0</property> + <child> + <object class="GtkCellRendererText" id="renderer3"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="combobox_past_future"> + <property name="visible">True</property> + <property name="model">model4</property> + <property name="active">0</property> + <child> + <object class="GtkCellRendererText" id="renderer4"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="padding">2</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="label" translatable="yes">label3</property> + <property name="justify">center</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + </object> +</interface> diff --git a/e-util/gal-a11y-e-cell-popup.c b/e-util/gal-a11y-e-cell-popup.c new file mode 100644 index 0000000000..523869bcb7 --- /dev/null +++ b/e-util/gal-a11y-e-cell-popup.c @@ -0,0 +1,153 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yang Wu <yang.wu@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-cell-popup.h" + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include "e-cell-popup.h" +#include "gal-a11y-e-cell-registry.h" +#include "gal-a11y-util.h" + +static AtkObjectClass *parent_class = NULL; +#define PARENT_TYPE (gal_a11y_e_cell_get_type ()) + +static void gal_a11y_e_cell_popup_class_init (GalA11yECellPopupClass *class); +static void popup_cell_action (GalA11yECell *cell); + +/** + * gal_a11y_e_cell_popup_get_type: + * @void: + * + * Registers the &GalA11yECellPopup class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yECellPopup class. + **/ +GType +gal_a11y_e_cell_popup_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yECellPopupClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) gal_a11y_e_cell_popup_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yECellPopup), + 0, + (GInstanceInitFunc) NULL, + NULL /* value_cell_popup */ + }; + + type = g_type_register_static (PARENT_TYPE, "GalA11yECellPopup", &info, 0); + gal_a11y_e_cell_type_add_action_interface (type); + } + + return type; +} + +static void +gal_a11y_e_cell_popup_class_init (GalA11yECellPopupClass *class) +{ + parent_class = g_type_class_ref (PARENT_TYPE); +} + +AtkObject * +gal_a11y_e_cell_popup_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row) +{ + AtkObject *a11y; + GalA11yECell *cell; + ECellPopup *popupcell; + ECellView * child_view = NULL; + + popupcell= E_CELL_POPUP (cell_view->ecell); + + if (popupcell && popupcell->popup_cell_view) + child_view = popupcell->popup_cell_view->child_view; + + if (child_view && child_view->ecell) { + a11y = gal_a11y_e_cell_registry_get_object ( + NULL, + item, + child_view, + parent, + model_col, + view_col, + row); + } else { + a11y = g_object_new (GAL_A11Y_TYPE_E_CELL_POPUP, NULL); + gal_a11y_e_cell_construct ( + a11y, + item, + cell_view, + parent, + model_col, + view_col, + row); + } + g_return_val_if_fail (a11y != NULL, NULL); + cell = GAL_A11Y_E_CELL (a11y); + gal_a11y_e_cell_add_action ( + cell, + "popup", + /* Translators: description of a "popup" action */ + _("popup a child"), + "<Alt>Down", /* action keybinding */ + popup_cell_action); + + a11y->role = ATK_ROLE_TABLE_CELL; + return a11y; +} + +static void +popup_cell_action (GalA11yECell *cell) +{ + gint finished; + GdkEvent event; + GtkLayout *layout; + + layout = GTK_LAYOUT (GNOME_CANVAS_ITEM (cell->item)->canvas); + + event.key.type = GDK_KEY_PRESS; + event.key.window = gtk_layout_get_bin_window (layout); + event.key.send_event = TRUE; + event.key.time = GDK_CURRENT_TIME; + event.key.state = GDK_MOD1_MASK; + event.key.keyval = GDK_KEY_Down; + + g_signal_emit_by_name (cell->item, "event", &event, &finished); +} diff --git a/e-util/gal-a11y-e-cell-popup.h b/e-util/gal-a11y-e-cell-popup.h new file mode 100644 index 0000000000..30ce4a7677 --- /dev/null +++ b/e-util/gal-a11y-e-cell-popup.h @@ -0,0 +1,65 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yang Wu <yang.wu@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_CELL_POPUP_H__ +#define __GAL_A11Y_E_CELL_POPUP_H__ + +#include <atk/atkgobjectaccessible.h> + +#include <e-util/e-table-item.h> +#include <e-util/gal-a11y-e-cell.h> + +#define GAL_A11Y_TYPE_E_CELL_POPUP (gal_a11y_e_cell_popup_get_type ()) +#define GAL_A11Y_E_CELL_POPUP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_CELL_POPUP, GalA11yECellPopup)) +#define GAL_A11Y_E_CELL_POPUP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_CELL_POPUP, GalA11yECellPopupClass)) +#define GAL_A11Y_IS_E_CELL_POPUP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_CELL_POPUP)) +#define GAL_A11Y_IS_E_CELL_POPUP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_CELL_POPUP)) + +typedef struct _GalA11yECellPopup GalA11yECellPopup; +typedef struct _GalA11yECellPopupClass GalA11yECellPopupClass; + +/* This struct should actually be larger as this isn't what we derive from. + * The GalA11yECellPopupPrivate comes right after the parent class structure. + **/ +struct _GalA11yECellPopup { + GalA11yECell object; +}; + +struct _GalA11yECellPopupClass { + GalA11yECellClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_cell_popup_get_type (void); +AtkObject *gal_a11y_e_cell_popup_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row); + +#endif /* __GAL_A11Y_E_CELL_POPUP_H__ */ diff --git a/e-util/gal-a11y-e-cell-registry.c b/e-util/gal-a11y-e-cell-registry.c new file mode 100644 index 0000000000..db05ac05c1 --- /dev/null +++ b/e-util/gal-a11y-e-cell-registry.c @@ -0,0 +1,151 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-cell.h" +#include "gal-a11y-e-cell-registry.h" + +static GObjectClass *parent_class; +static GalA11yECellRegistry *default_registry; +#define PARENT_TYPE (G_TYPE_OBJECT) + +struct _GalA11yECellRegistryPrivate { + GHashTable *table; +}; + +/* Static functions */ + +static void +gal_a11y_e_cell_registry_finalize (GObject *obj) +{ + GalA11yECellRegistry *registry = GAL_A11Y_E_CELL_REGISTRY (obj); + + g_hash_table_destroy (registry->priv->table); + g_free (registry->priv); + + G_OBJECT_CLASS (parent_class)->finalize (obj); +} + +static void +gal_a11y_e_cell_registry_class_init (GalA11yECellRegistryClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + object_class->finalize = gal_a11y_e_cell_registry_finalize; +} + +static void +gal_a11y_e_cell_registry_init (GalA11yECellRegistry *registry) +{ + registry->priv = g_new (GalA11yECellRegistryPrivate, 1); + registry->priv->table = g_hash_table_new (NULL, NULL); +} + +/** + * gal_a11y_e_cell_registry_get_type: + * @void: + * + * Registers the &GalA11yECellRegistry class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yECellRegistry class. + **/ +GType +gal_a11y_e_cell_registry_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yECellRegistryClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) gal_a11y_e_cell_registry_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yECellRegistry), + 0, + (GInstanceInitFunc) gal_a11y_e_cell_registry_init, + NULL /* value_cell */ + }; + + type = g_type_register_static ( + PARENT_TYPE, "GalA11yECellRegistry", &info, 0); + } + + return type; +} + +static void +init_default_registry (void) +{ + if (default_registry == NULL) { + default_registry = g_object_new (gal_a11y_e_cell_registry_get_type (), NULL); + } +} + +AtkObject * +gal_a11y_e_cell_registry_get_object (GalA11yECellRegistry *registry, + ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row) +{ + GalA11yECellRegistryFunc func = NULL; + GType type; + + if (registry == NULL) { + init_default_registry (); + registry = default_registry; + } + + type = G_OBJECT_TYPE (cell_view->ecell); + while (func == NULL && type != 0) { + func = g_hash_table_lookup (registry->priv->table, GINT_TO_POINTER (type)); + type = g_type_parent (type); + } + + if (func == NULL) + func = gal_a11y_e_cell_new; + + return func (item, cell_view, parent, model_col, view_col, row); +} + +void +gal_a11y_e_cell_registry_add_cell_type (GalA11yECellRegistry *registry, + GType type, + GalA11yECellRegistryFunc func) +{ + if (registry == NULL) { + init_default_registry (); + registry = default_registry; + } + + g_hash_table_insert (registry->priv->table, GINT_TO_POINTER (type), func); +} diff --git a/e-util/gal-a11y-e-cell-registry.h b/e-util/gal-a11y-e-cell-registry.h new file mode 100644 index 0000000000..fdfd9dcffd --- /dev/null +++ b/e-util/gal-a11y-e-cell-registry.h @@ -0,0 +1,75 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_CELL_REGISTRY_H__ +#define __GAL_A11Y_E_CELL_REGISTRY_H__ + +#include <atk/atkobject.h> + +#include <e-util/e-table-item.h> +#include <e-util/e-cell.h> + +#define GAL_A11Y_TYPE_E_CELL_REGISTRY (gal_a11y_e_cell_registry_get_type ()) +#define GAL_A11Y_E_CELL_REGISTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_CELL_REGISTRY, GalA11yECellRegistry)) +#define GAL_A11Y_E_CELL_REGISTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_CELL_REGISTRY, GalA11yECellRegistryClass)) +#define GAL_A11Y_IS_E_CELL_REGISTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_CELL_REGISTRY)) +#define GAL_A11Y_IS_E_CELL_REGISTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_CELL_REGISTRY)) + +typedef struct _GalA11yECellRegistry GalA11yECellRegistry; +typedef struct _GalA11yECellRegistryClass GalA11yECellRegistryClass; +typedef struct _GalA11yECellRegistryPrivate GalA11yECellRegistryPrivate; + +typedef AtkObject *(*GalA11yECellRegistryFunc) (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row); + +struct _GalA11yECellRegistry { + GObject object; + + GalA11yECellRegistryPrivate *priv; +}; + +struct _GalA11yECellRegistryClass { + GObjectClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_cell_registry_get_type (void); +AtkObject *gal_a11y_e_cell_registry_get_object (GalA11yECellRegistry *registry, + ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row); +void gal_a11y_e_cell_registry_add_cell_type (GalA11yECellRegistry *registry, + GType type, + GalA11yECellRegistryFunc func); + +#endif /* __GAL_A11Y_E_CELL_REGISTRY_H__ */ diff --git a/e-util/gal-a11y-e-cell-toggle.c b/e-util/gal-a11y-e-cell-toggle.c new file mode 100644 index 0000000000..8be7f44122 --- /dev/null +++ b/e-util/gal-a11y-e-cell-toggle.c @@ -0,0 +1,198 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-cell-toggle.h" + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "e-cell-toggle.h" +#include "e-table-model.h" + +#define PARENT_TYPE (gal_a11y_e_cell_get_type ()) +static GObjectClass *parent_class; + +static void gal_a11y_e_cell_toggle_class_init (GalA11yECellToggleClass *class); + +static void +gal_a11y_e_cell_toggle_dispose (GObject *object) +{ + GalA11yECellToggle *a11y = GAL_A11Y_E_CELL_TOGGLE (object); + + ETableModel *e_table_model = GAL_A11Y_E_CELL (a11y)->item->table_model; + + if (e_table_model && a11y->model_id > 0) { + g_signal_handler_disconnect (e_table_model, a11y->model_id); + a11y->model_id = 0; + } + + if (parent_class->dispose) + parent_class->dispose (object); +} + +GType +gal_a11y_e_cell_toggle_get_type (void) +{ + static GType type = 0; + + if (!type) + { + static const GTypeInfo tinfo = + { + sizeof (GalA11yECellToggleClass), + (GBaseInitFunc) NULL, /* base init */ + (GBaseFinalizeFunc) NULL, /* base finalize */ + (GClassInitFunc) gal_a11y_e_cell_toggle_class_init, /* class init */ + (GClassFinalizeFunc) NULL, /* class finalize */ + NULL, /* class data */ + sizeof (GalA11yECellToggle), /* instance size */ + 0, /* nb preallocs */ + NULL, /* instance init */ + NULL /* value table */ + }; + + type = g_type_register_static (GAL_A11Y_TYPE_E_CELL, + "GalA11yECellToggle", &tinfo, 0); + gal_a11y_e_cell_type_add_action_interface (type); + + } + return type; +} + +static void +gal_a11y_e_cell_toggle_class_init (GalA11yECellToggleClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = gal_a11y_e_cell_toggle_dispose; + parent_class = g_type_class_ref (PARENT_TYPE); +} + +static void +toggle_cell_action (GalA11yECell *cell) +{ + gint finished; + GtkLayout *layout; + GdkEventButton event; + gint x, y, width, height; + gint row, col; + + row = cell->row; + col = cell->view_col; + + layout = GTK_LAYOUT (GNOME_CANVAS_ITEM (cell->item)->canvas); + + e_table_item_get_cell_geometry ( + cell->item, &row, &col, &x, &y, &width, &height); + + event.x = x + width / 2 + (gint)(GNOME_CANVAS_ITEM (cell->item)->x1); + event.y = y + height / 2 + (gint)(GNOME_CANVAS_ITEM (cell->item)->y1); + + event.type = GDK_BUTTON_PRESS; + event.window = gtk_layout_get_bin_window (layout); + event.button = 1; + event.send_event = TRUE; + event.time = GDK_CURRENT_TIME; + event.axes = NULL; + + g_signal_emit_by_name (cell->item, "event", &event, &finished); +} + +static void +model_change_cb (ETableModel *etm, + gint col, + gint row, + GalA11yECell *cell) +{ + gint value; + + if (col == cell->model_col && row == cell->row) { + + value = GPOINTER_TO_INT ( + e_table_model_value_at (cell->cell_view->e_table_model, + cell->model_col, cell->row)); + /* Cheat gnopernicus, or it will ignore the state change signal */ + atk_focus_tracker_notify (ATK_OBJECT (cell)); + + if (value) + gal_a11y_e_cell_add_state (cell, ATK_STATE_CHECKED, TRUE); + else + gal_a11y_e_cell_remove_state (cell, ATK_STATE_CHECKED, TRUE); + } +} + +AtkObject * +gal_a11y_e_cell_toggle_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row) +{ + AtkObject *a11y; + GalA11yECell *cell; + GalA11yECellToggle *toggle_cell; + gint value; + + a11y = ATK_OBJECT (g_object_new (GAL_A11Y_TYPE_E_CELL_TOGGLE, NULL)); + + g_return_val_if_fail (a11y != NULL, NULL); + + cell = GAL_A11Y_E_CELL (a11y); + toggle_cell = GAL_A11Y_E_CELL_TOGGLE (a11y); + a11y->role = ATK_ROLE_TABLE_CELL; + + gal_a11y_e_cell_construct ( + a11y, + item, + cell_view, + parent, + model_col, + view_col, + row); + + gal_a11y_e_cell_add_action ( + cell, + "toggle", + /* Translators: description of a "toggle" action */ + _("toggle the cell"), + NULL, /* action keybinding */ + toggle_cell_action); + + toggle_cell->model_id = g_signal_connect ( + item->table_model, "model_cell_changed", + (GCallback) model_change_cb, a11y); + + value = GPOINTER_TO_INT ( + e_table_model_value_at ( + cell->cell_view->e_table_model, + cell->model_col, cell->row)); + if (value) + gal_a11y_e_cell_add_state (cell, ATK_STATE_CHECKED, FALSE); + else + gal_a11y_e_cell_remove_state (cell, ATK_STATE_CHECKED, FALSE); + + return a11y; +} diff --git a/e-util/gal-a11y-e-cell-toggle.h b/e-util/gal-a11y-e-cell-toggle.h new file mode 100644 index 0000000000..bd3670edda --- /dev/null +++ b/e-util/gal-a11y-e-cell-toggle.h @@ -0,0 +1,67 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_CELL_TOGGLE_H__ +#define __GAL_A11Y_E_CELL_TOGGLE_H__ + +#include <atk/atk.h> +#include "gal-a11y-e-cell.h" +#include "gal-a11y-e-cell-toggle.h" + +G_BEGIN_DECLS + +#define GAL_A11Y_TYPE_E_CELL_TOGGLE (gal_a11y_e_cell_toggle_get_type ()) +#define GAL_A11Y_E_CELL_TOGGLE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_CELL_TOGGLE, GalA11yECellToggle)) +#define GAL_A11Y_E_CELL_TOGGLE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_E_CELL_TOGGLE, GalA11yECellToggleClass)) +#define GAL_A11Y_IS_E_CELL_TOGGLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_CELL_TOGGLE)) +#define GAL_A11Y_IS_E_CELL_TOGGLE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_CELL_TOGGLE)) +#define GAL_A11Y_E_CELL_TOGGLE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GAL_A11Y_TYPE_E_CELL_TOGGLE, GalA11yECellToggleClass)) + +typedef struct _GalA11yECellToggle GalA11yECellToggle; +typedef struct _GalA11yECellToggleClass GalA11yECellToggleClass; + +struct _GalA11yECellToggle +{ + GalA11yECell parent; + gint model_id; +}; + +GType gal_a11y_e_cell_toggle_get_type (void); + +struct _GalA11yECellToggleClass +{ + GalA11yECellClass parent_class; +}; + +AtkObject *gal_a11y_e_cell_toggle_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row); + +G_END_DECLS + +#endif /* __GAL_A11Y_E_CELL_TOGGLE_H__ */ diff --git a/e-util/gal-a11y-e-cell-tree.c b/e-util/gal-a11y-e-cell-tree.c new file mode 100644 index 0000000000..e0757f5300 --- /dev/null +++ b/e-util/gal-a11y-e-cell-tree.c @@ -0,0 +1,266 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Tim Wo <tim.wo@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-cell-tree.h" + +#include <atk/atk.h> +#include <glib/gi18n.h> + +#include "e-cell-tree.h" +#include "e-table.h" +#include "e-tree-table-adapter.h" +#include "gal-a11y-e-cell-registry.h" +#include "gal-a11y-util.h" + +#define CS_CLASS(a11y) (G_TYPE_INSTANCE_GET_CLASS ((a11y), C_TYPE_STREAM, GalA11yECellTreeClass)) +static AtkObjectClass *a11y_parent_class; +#define A11Y_PARENT_TYPE (gal_a11y_e_cell_get_type ()) + +#define d(x) + +static void +ectr_model_row_changed_cb (ETableModel *etm, + gint row, + GalA11yECell *a11y) +{ + ETreePath node; + ETreeModel *tree_model; + ETreeTableAdapter *tree_table_adapter; + + g_return_if_fail (a11y); + if (a11y->row != row) + return; + + node = e_table_model_value_at (etm, -1, a11y->row); + tree_model = e_table_model_value_at (etm, -2, a11y->row); + tree_table_adapter = e_table_model_value_at (etm, -3, a11y->row); + + if (e_tree_model_node_is_expandable (tree_model, node)) { + gboolean is_exp = e_tree_table_adapter_node_is_expanded (tree_table_adapter, node); + if (is_exp) + gal_a11y_e_cell_add_state (a11y, ATK_STATE_EXPANDED, TRUE); + else + gal_a11y_e_cell_remove_state (a11y, ATK_STATE_EXPANDED, TRUE); + } +} + +static void +kill_view_cb (ECellView *subcell_view, + gpointer psubcell_a11ies) +{ + GList *node; + GList *subcell_a11ies = (GList *) psubcell_a11ies; + GalA11yECell *subcell; + + for (node = subcell_a11ies; node != NULL; node = g_list_next (node)) + { + subcell = GAL_A11Y_E_CELL (node->data); + if (subcell && subcell->cell_view == subcell_view) + { + d (fprintf (stderr, "subcell_view %p deleted before the a11y object %p\n", subcell_view, subcell)); + subcell->cell_view = NULL; + } + } +} + +static void +ectr_subcell_weak_ref (GalA11yECellTree *a11y, + GalA11yECell *subcell_a11y) +{ + ECellView *subcell_view = subcell_a11y ? subcell_a11y->cell_view : NULL; + if (subcell_a11y && subcell_view && subcell_view->kill_view_cb_data) + subcell_view->kill_view_cb_data = g_list_remove (subcell_view->kill_view_cb_data, subcell_a11y); + + g_signal_handler_disconnect ( + GAL_A11Y_E_CELL (a11y)->item->table_model, + a11y->model_row_changed_id); + g_object_unref (a11y); +} + +static void +ectr_do_action_expand (AtkAction *action) +{ + GalA11yECell *a11y; + ETableModel *table_model; + ETreePath node; + ETreeModel *tree_model; + ETreeTableAdapter *tree_table_adapter; + + a11y = GAL_A11Y_E_CELL (action); + table_model = a11y->item->table_model; + node = e_table_model_value_at (table_model, -1, a11y->row); + tree_model = e_table_model_value_at (table_model, -2, a11y->row); + tree_table_adapter = e_table_model_value_at (table_model, -3, a11y->row); + + if (e_tree_model_node_is_expandable (tree_model, node)) { + e_tree_table_adapter_node_set_expanded ( + tree_table_adapter, node, TRUE); + gal_a11y_e_cell_add_state (a11y, ATK_STATE_EXPANDED, TRUE); + } +} + +static void +ectr_do_action_collapse (AtkAction *action) +{ + GalA11yECell *a11y; + ETableModel *table_model; + ETreePath node; + ETreeModel *tree_model; + ETreeTableAdapter *tree_table_adapter; + + a11y = GAL_A11Y_E_CELL (action); + table_model = a11y->item->table_model; + node = e_table_model_value_at (table_model, -1, a11y->row); + tree_model = e_table_model_value_at (table_model, -2, a11y->row); + tree_table_adapter = e_table_model_value_at (table_model, -3, a11y->row); + + if (e_tree_model_node_is_expandable (tree_model, node)) { + e_tree_table_adapter_node_set_expanded ( + tree_table_adapter, node, FALSE); + gal_a11y_e_cell_remove_state (a11y, ATK_STATE_EXPANDED, TRUE); + } +} + +static void +ectr_class_init (GalA11yECellTreeClass *class) +{ + a11y_parent_class = g_type_class_ref (A11Y_PARENT_TYPE); +} + +static void +ectr_init (GalA11yECellTree *a11y) +{ +} + +GType +gal_a11y_e_cell_tree_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yECellTreeClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) ectr_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yECellTree), + 0, + (GInstanceInitFunc) ectr_init, + NULL /* value_cell_text */ + }; + + type = g_type_register_static (A11Y_PARENT_TYPE, "GalA11yECellTree", &info, 0); + gal_a11y_e_cell_type_add_action_interface (type); + } + + return type; +} + +AtkObject * +gal_a11y_e_cell_tree_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row) +{ + AtkObject *subcell_a11y; + GalA11yECellTree *a11y; + + ETreePath node; + ETreeModel *tree_model; + ETreeTableAdapter *tree_table_adapter; + + ECellView *subcell_view; + subcell_view = e_cell_tree_view_get_subcell_view (cell_view); + + if (subcell_view->ecell) { + subcell_a11y = gal_a11y_e_cell_registry_get_object ( + NULL, + item, + subcell_view, + parent, + model_col, + view_col, + row); + gal_a11y_e_cell_add_action ( + GAL_A11Y_E_CELL (subcell_a11y), + "expand", + /* Translators: description of an "expand" action */ + _("expands the row in the ETree containing this cell"), + NULL, + (ACTION_FUNC) ectr_do_action_expand); + + gal_a11y_e_cell_add_action ( + GAL_A11Y_E_CELL (subcell_a11y), + "collapse", + /* Translators: description of a "collapse" action */ + _("collapses the row in the ETree containing this cell"), + NULL, + (ACTION_FUNC) ectr_do_action_collapse); + + /* init AtkStates for the cell's a11y object */ + node = e_table_model_value_at (item->table_model, -1, row); + tree_model = e_table_model_value_at (item->table_model, -2, row); + tree_table_adapter = e_table_model_value_at (item->table_model, -3, row); + if (e_tree_model_node_is_expandable (tree_model, node)) { + gal_a11y_e_cell_add_state (GAL_A11Y_E_CELL (subcell_a11y), ATK_STATE_EXPANDABLE, FALSE); + if (e_tree_table_adapter_node_is_expanded (tree_table_adapter, node)) + gal_a11y_e_cell_add_state (GAL_A11Y_E_CELL (subcell_a11y), ATK_STATE_EXPANDED, FALSE); + } + } + else + subcell_a11y = NULL; + + /* create a companion a11y object, this object has type GalA11yECellTree + * and it connects to some signals to determine whether a tree cell is + * expanded or collapsed */ + a11y = g_object_new (gal_a11y_e_cell_tree_get_type (), NULL); + gal_a11y_e_cell_construct ( + ATK_OBJECT (a11y), + item, + cell_view, + parent, + model_col, + view_col, + row); + a11y->model_row_changed_id = g_signal_connect ( + item->table_model, "model_row_changed", + G_CALLBACK (ectr_model_row_changed_cb), subcell_a11y); + + if (subcell_a11y && subcell_view) + { + subcell_view->kill_view_cb = kill_view_cb; + if (!g_list_find (subcell_view->kill_view_cb_data, subcell_a11y)) + subcell_view->kill_view_cb_data = g_list_append (subcell_view->kill_view_cb_data, subcell_a11y); + } + + g_object_weak_ref (G_OBJECT (subcell_a11y), (GWeakNotify) ectr_subcell_weak_ref, a11y); + + return subcell_a11y; +} diff --git a/e-util/gal-a11y-e-cell-tree.h b/e-util/gal-a11y-e-cell-tree.h new file mode 100644 index 0000000000..caa5f4034a --- /dev/null +++ b/e-util/gal-a11y-e-cell-tree.h @@ -0,0 +1,66 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Tim Wo <tim.wo@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_CELL_TREE_H__ +#define __GAL_A11Y_E_CELL_TREE_H__ + +#include <e-util/e-table-item.h> +#include <e-util/e-cell-tree.h> +#include <e-util/gal-a11y-e-cell.h> + +#define GAL_A11Y_TYPE_E_CELL_TREE (gal_a11y_e_cell_tree_get_type ()) +#define GAL_A11Y_E_CELL_TREE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_CELL_TREE, GalA11yECellTree)) +#define GAL_A11Y_E_CELL_TREE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_CELL_TREE, GalA11yECellTreeClass)) +#define GAL_A11Y_IS_E_CELL_TREE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_CELL_TREE)) +#define GAL_A11Y_IS_E_CELL_TREE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_CELL_TREE)) + +typedef struct _GalA11yECellTree GalA11yECellTree; +typedef struct _GalA11yECellTreeClass GalA11yECellTreeClass; +typedef struct _GalA11yECellTreePrivate GalA11yECellTreePrivate; + +/* This struct should actually be larger as this isn't what we derive from. + * The GalA11yECellTreePrivate comes right after the parent class structure. + **/ +struct _GalA11yECellTree { + GalA11yECell object; + + gint model_row_changed_id; +}; + +struct _GalA11yECellTreeClass { + GalA11yECellClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_cell_tree_get_type (void); +AtkObject *gal_a11y_e_cell_tree_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row); + +#endif /* __GAL_A11Y_E_CELL_TREE_H__ */ diff --git a/e-util/gal-a11y-e-cell-vbox.c b/e-util/gal-a11y-e-cell-vbox.c new file mode 100644 index 0000000000..7864dc04ea --- /dev/null +++ b/e-util/gal-a11y-e-cell-vbox.c @@ -0,0 +1,235 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Eric Zhao <eric.zhao@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * Copyright (C) 2004 Sun Microsystem, Inc. + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-cell-vbox.h" + +#include <atk/atk.h> + +#include "e-cell-vbox.h" +#include "gal-a11y-e-cell-registry.h" + +static GObjectClass *parent_class; +static AtkComponentIface *component_parent_iface; +#define PARENT_TYPE (gal_a11y_e_cell_get_type ()) + +static gint +ecv_get_n_children (AtkObject *a11y) +{ + g_return_val_if_fail (GAL_A11Y_IS_E_CELL_VBOX (a11y), 0); + + return GAL_A11Y_E_CELL_VBOX (a11y)->a11y_subcell_count; +} + +static void +subcell_destroyed (gpointer data) +{ + GalA11yECell *cell; + AtkObject *parent; + GalA11yECellVbox *gaev; + + g_return_if_fail (GAL_A11Y_IS_E_CELL (data)); + cell = GAL_A11Y_E_CELL (data); + + parent = atk_object_get_parent (ATK_OBJECT (cell)); + g_return_if_fail (GAL_A11Y_IS_E_CELL_VBOX (parent)); + gaev = GAL_A11Y_E_CELL_VBOX (parent); + + if (cell->view_col < gaev->a11y_subcell_count) + gaev->a11y_subcells[cell->view_col] = NULL; +} + +static AtkObject * +ecv_ref_child (AtkObject *a11y, + gint i) +{ + GalA11yECellVbox *gaev = GAL_A11Y_E_CELL_VBOX (a11y); + GalA11yECell *gaec = GAL_A11Y_E_CELL (a11y); + ECellVboxView *ecvv = (ECellVboxView *) (gaec->cell_view); + AtkObject *ret; + if (i < gaev->a11y_subcell_count) { + if (gaev->a11y_subcells[i] == NULL) { + ECellView *subcell_view; + gint model_col, row; + row = gaec->row; + model_col = ecvv->model_cols[i]; + subcell_view = ecvv->subcell_views[i]; + /* FIXME Should the view column use a fake + * one or the same as its parent? */ + ret = gal_a11y_e_cell_registry_get_object ( + NULL, + gaec->item, + subcell_view, + a11y, + model_col, + gaec->view_col, + row); + gaev->a11y_subcells[i] = ret; + g_object_ref (ret); + g_object_weak_ref ( + G_OBJECT (ret), + (GWeakNotify) subcell_destroyed, + ret); + } else { + ret = (AtkObject *) gaev->a11y_subcells[i]; + if (ATK_IS_OBJECT (ret)) + g_object_ref (ret); + else + ret = NULL; + } + } else { + ret = NULL; + } + + return ret; +} + +static void +ecv_dispose (GObject *object) +{ + GalA11yECellVbox *gaev = GAL_A11Y_E_CELL_VBOX (object); + if (gaev->a11y_subcells) + g_free (gaev->a11y_subcells); + + if (parent_class->dispose) + parent_class->dispose (object); +} + +/* AtkComponet interface */ +static AtkObject * +ecv_ref_accessible_at_point (AtkComponent *component, + gint x, + gint y, + AtkCoordType coord_type) +{ + gint x0, y0, width, height; + gint subcell_height, i; + + GalA11yECell *gaec = GAL_A11Y_E_CELL (component); + ECellVboxView *ecvv = (ECellVboxView *) (gaec->cell_view); + + atk_component_get_extents (component, &x0, &y0, &width, &height, coord_type); + x -= x0; + y -= y0; + if (x < 0 || x > width || y < 0 || y > height) + return NULL; + + for (i = 0; i < ecvv->subcell_view_count; i++) { + subcell_height = e_cell_height ( + ecvv->subcell_views[i], ecvv->model_cols[i], + gaec->view_col, gaec->row); + if (0 <= y && y <= subcell_height) { + return ecv_ref_child ((AtkObject *) component, i); + } else + y -= subcell_height; + } + + return NULL; +} + +static void +ecv_class_init (GalA11yECellVboxClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + AtkObjectClass *a11y_class = ATK_OBJECT_CLASS (class); + parent_class = g_type_class_ref (PARENT_TYPE); + + object_class->dispose = ecv_dispose; + + a11y_class->get_n_children = ecv_get_n_children; + a11y_class->ref_child = ecv_ref_child; +} + +static void +ecv_init (GalA11yECellVbox *a11y) +{ +} + +static void +ecv_atk_component_iface_init (AtkComponentIface *iface) +{ + component_parent_iface = g_type_interface_peek_parent (iface); + + iface->ref_accessible_at_point = ecv_ref_accessible_at_point; +} + +GType +gal_a11y_e_cell_vbox_get_type (void) +{ + static GType type = 0; + if (!type) { + GTypeInfo info = { + sizeof (GalA11yECellVboxClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) ecv_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yECellVbox), + 0, + (GInstanceInitFunc) ecv_init, + NULL /* value_cell */ + }; + + static const GInterfaceInfo atk_component_info = { + (GInterfaceInitFunc) ecv_atk_component_iface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + type = g_type_register_static (PARENT_TYPE, "GalA11yECellVbox", &info, 0); + gal_a11y_e_cell_type_add_action_interface (type); + g_type_add_interface_static (type, ATK_TYPE_COMPONENT, &atk_component_info); + } + + return type; +} + +AtkObject *gal_a11y_e_cell_vbox_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row) +{ + AtkObject *a11y; + GalA11yECell *gaec; + GalA11yECellVbox *gaev; + ECellVboxView *ecvv; + + a11y = g_object_new (gal_a11y_e_cell_vbox_get_type (), NULL); + + gal_a11y_e_cell_construct ( + a11y, item, cell_view, parent, model_col, view_col, row); + + gaec = GAL_A11Y_E_CELL (a11y); + gaev = GAL_A11Y_E_CELL_VBOX (a11y); + ecvv = (ECellVboxView *) (gaec->cell_view); + gaev->a11y_subcell_count = ecvv->subcell_view_count; + gaev->a11y_subcells = g_malloc0 (sizeof (AtkObject *) * gaev->a11y_subcell_count); + return a11y; +} diff --git a/e-util/gal-a11y-e-cell-vbox.h b/e-util/gal-a11y-e-cell-vbox.h new file mode 100644 index 0000000000..cb6807e0a4 --- /dev/null +++ b/e-util/gal-a11y-e-cell-vbox.h @@ -0,0 +1,67 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Eric Zhao <eric.zhao@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * Copyright (C) 2004 Sun Microsystem, Inc. + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_CELL_VBOX_H__ +#define __GAL_A11Y_E_CELL_VBOX_H__ + +#include "gal-a11y-e-cell.h" + +G_BEGIN_DECLS + +#define GAL_A11Y_TYPE_E_CELL_VBOX (gal_a11y_e_cell_vbox_get_type ()) +#define GAL_A11Y_E_CELL_VBOX(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_CELL_VBOX, GalA11yECellVbox)) +#define GAL_A11Y_E_CELL_VBOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_E_CELL_VBOX, GalA11yECellVboxClass)) +#define GAL_A11Y_IS_E_CELL_VBOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_CELL_VBOX)) +#define GAL_A11Y_IS_E_CELL_VBOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_CELL_VBOX)) +#define GAL_A11Y_E_CELL_VBOX_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GAL_A11Y_TYPE_E_CELL_VBOX, GalA11yECellVboxClass)) + +typedef struct _GalA11yECellVbox GalA11yECellVbox; +typedef struct _GalA11yECellVboxClass GalA11yECellVboxClass; + +struct _GalA11yECellVbox +{ + GalA11yECell object; + gint a11y_subcell_count; + gpointer *a11y_subcells; +}; + +struct _GalA11yECellVboxClass +{ + GalA11yECellClass parent_class; +}; + +GType gal_a11y_e_cell_vbox_get_type (void); +AtkObject *gal_a11y_e_cell_vbox_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row); + +G_END_DECLS +#endif /* __GAL_A11Y_E_CELL_VBOX_H__ */ diff --git a/e-util/gal-a11y-e-cell.c b/e-util/gal-a11y-e-cell.c new file mode 100644 index 0000000000..9f16b34fd9 --- /dev/null +++ b/e-util/gal-a11y-e-cell.c @@ -0,0 +1,648 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-cell.h" + +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "e-table.h" +#include "e-tree.h" +#include "gal-a11y-e-cell-vbox.h" +#include "gal-a11y-e-table-item.h" +#include "gal-a11y-util.h" + +static GObjectClass *parent_class; +#define PARENT_TYPE (atk_object_get_type ()) + +#if 0 +static void +unref_item (gpointer user_data, + GObject *obj_loc) +{ + GalA11yECell *a11y = GAL_A11Y_E_CELL (user_data); + a11y->item = NULL; + g_object_unref (a11y); +} + +static void +unref_cell (gpointer user_data, + GObject *obj_loc) +{ + GalA11yECell *a11y = GAL_A11Y_E_CELL (user_data); + a11y->cell_view = NULL; + g_object_unref (a11y); +} +#endif + +static gboolean +is_valid (AtkObject *cell) +{ + GalA11yECell *a11y = GAL_A11Y_E_CELL (cell); + GalA11yETableItem *a11yItem = GAL_A11Y_E_TABLE_ITEM (a11y->parent); + AtkStateSet *item_ss; + gboolean ret = TRUE; + + item_ss = atk_object_ref_state_set (ATK_OBJECT (a11yItem)); + if (atk_state_set_contains_state (item_ss, ATK_STATE_DEFUNCT)) + ret = FALSE; + + g_object_unref (item_ss); + + if (ret && atk_state_set_contains_state (a11y->state_set, ATK_STATE_DEFUNCT)) + ret = FALSE; + + return ret; +} + +static void +gal_a11y_e_cell_dispose (GObject *object) +{ + GalA11yECell *a11y = GAL_A11Y_E_CELL (object); + +#if 0 + if (a11y->item) + g_object_unref (a11y->item); /*, unref_item, a11y); */ + if (a11y->cell_view) + g_object_unref (a11y->cell_view); /*, unref_cell, a11y); */ + if (a11y->parent) + g_object_unref (a11y->parent); +#endif + + if (a11y->state_set) { + g_object_unref (a11y->state_set); + a11y->state_set = NULL; + } + + if (parent_class->dispose) + parent_class->dispose (object); + +} + +/* Static functions */ +static const gchar * +gal_a11y_e_cell_get_name (AtkObject *a11y) +{ + GalA11yECell *cell = GAL_A11Y_E_CELL (a11y); + ETableCol *ecol; + + if (a11y->name != NULL && strcmp (a11y->name, "")) + return a11y->name; + + if (cell->item != NULL) { + ecol = e_table_header_get_column (cell->item->header, cell->view_col); + if (ecol != NULL) + return ecol->text; + } + + return _("Table Cell"); +} + +static AtkStateSet * +gal_a11y_e_cell_ref_state_set (AtkObject *accessible) +{ + GalA11yECell *cell = GAL_A11Y_E_CELL (accessible); + + g_return_val_if_fail (cell->state_set, NULL); + + g_object_ref (cell->state_set); + + return cell->state_set; +} + +static AtkObject * +gal_a11y_e_cell_get_parent (AtkObject *accessible) +{ + GalA11yECell *a11y = GAL_A11Y_E_CELL (accessible); + return a11y->parent; +} + +static gint +gal_a11y_e_cell_get_index_in_parent (AtkObject *accessible) +{ + GalA11yECell *a11y = GAL_A11Y_E_CELL (accessible); + + if (!is_valid (accessible)) + return -1; + + return (a11y->row + 1) * a11y->item->cols + a11y->view_col; +} + +/* Component IFace */ +static void +gal_a11y_e_cell_get_extents (AtkComponent *component, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coord_type) +{ + GalA11yECell *a11y = GAL_A11Y_E_CELL (component); + GtkWidget *tableOrTree; + gint row; + gint col; + gint xval; + gint yval; + + row = a11y->row; + col = a11y->view_col; + + tableOrTree = gtk_widget_get_parent (GTK_WIDGET (a11y->item->parent.canvas)); + if (E_IS_TREE (tableOrTree)) { + e_tree_get_cell_geometry ( + E_TREE (tableOrTree), + row, col, &xval, &yval, + width, height); + } else { + e_table_get_cell_geometry ( + E_TABLE (tableOrTree), + row, col, &xval, &yval, + width, height); + } + + atk_component_get_position ( + ATK_COMPONENT (a11y->parent), + x, y, coord_type); + if (x && *x != G_MININT) + *x += xval; + if (y && *y != G_MININT) + *y += yval; +} + +static gboolean +gal_a11y_e_cell_grab_focus (AtkComponent *component) +{ + GalA11yECell *a11y; + gint index; + GtkWidget *toplevel; + GalA11yETableItem *a11yTableItem; + + a11y = GAL_A11Y_E_CELL (component); + + /* for e_cell_vbox's children, we just grab the e_cell_vbox */ + if (GAL_A11Y_IS_E_CELL_VBOX (a11y->parent)) { + return atk_component_grab_focus (ATK_COMPONENT (a11y->parent)); + } + + a11yTableItem = GAL_A11Y_E_TABLE_ITEM (a11y->parent); + index = atk_object_get_index_in_parent (ATK_OBJECT (a11y)); + + atk_selection_clear_selection (ATK_SELECTION (a11yTableItem)); + atk_selection_add_selection (ATK_SELECTION (a11yTableItem), index); + + gtk_widget_grab_focus ( + GTK_WIDGET (GNOME_CANVAS_ITEM (a11y->item)->canvas)); + toplevel = gtk_widget_get_toplevel ( + GTK_WIDGET (GNOME_CANVAS_ITEM (a11y->item)->canvas)); + if (toplevel && gtk_widget_is_toplevel (toplevel)) + gtk_window_present (GTK_WINDOW (toplevel)); + + return TRUE; +} + +/* Table IFace */ + +static void +gal_a11y_e_cell_atk_component_iface_init (AtkComponentIface *iface) +{ + iface->get_extents = gal_a11y_e_cell_get_extents; + iface->grab_focus = gal_a11y_e_cell_grab_focus; +} + +static void +gal_a11y_e_cell_class_init (GalA11yECellClass *class) +{ + AtkObjectClass *atk_object_class = ATK_OBJECT_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + object_class->dispose = gal_a11y_e_cell_dispose; + + atk_object_class->get_parent = gal_a11y_e_cell_get_parent; + atk_object_class->get_index_in_parent = gal_a11y_e_cell_get_index_in_parent; + atk_object_class->ref_state_set = gal_a11y_e_cell_ref_state_set; + atk_object_class->get_name = gal_a11y_e_cell_get_name; +} + +static void +gal_a11y_e_cell_init (GalA11yECell *a11y) +{ + a11y->item = NULL; + a11y->cell_view = NULL; + a11y->parent = NULL; + a11y->model_col = -1; + a11y->view_col = -1; + a11y->row = -1; + + a11y->state_set = atk_state_set_new (); + atk_state_set_add_state (a11y->state_set, ATK_STATE_TRANSIENT); + atk_state_set_add_state (a11y->state_set, ATK_STATE_ENABLED); + atk_state_set_add_state (a11y->state_set, ATK_STATE_SENSITIVE); + atk_state_set_add_state (a11y->state_set, ATK_STATE_SELECTABLE); + atk_state_set_add_state (a11y->state_set, ATK_STATE_SHOWING); + atk_state_set_add_state (a11y->state_set, ATK_STATE_FOCUSABLE); + atk_state_set_add_state (a11y->state_set, ATK_STATE_VISIBLE); +} + +static ActionInfo * +_gal_a11y_e_cell_get_action_info (GalA11yECell *cell, + gint index) +{ + GList *list_node; + + g_return_val_if_fail (GAL_A11Y_IS_E_CELL (cell), NULL); + if (cell->action_list == NULL) + return NULL; + list_node = g_list_nth (cell->action_list, index); + if (!list_node) + return NULL; + return (ActionInfo *) (list_node->data); +} + +static void +_gal_a11y_e_cell_destroy_action_info (gpointer action_info, + gpointer user_data) +{ + ActionInfo *info = (ActionInfo *) action_info; + + g_return_if_fail (info != NULL); + g_free (info->name); + g_free (info->description); + g_free (info->keybinding); + g_free (info); +} + +gboolean +gal_a11y_e_cell_add_action (GalA11yECell *cell, + const gchar *action_name, + const gchar *action_description, + const gchar *action_keybinding, + ACTION_FUNC action_func) +{ + ActionInfo *info; + g_return_val_if_fail (GAL_A11Y_IS_E_CELL (cell), FALSE); + info = g_new (ActionInfo, 1); + + if (action_name != NULL) + info->name = g_strdup (action_name); + else + info->name = NULL; + + if (action_description != NULL) + info->description = g_strdup (action_description); + else + info->description = NULL; + if (action_keybinding != NULL) + info->keybinding = g_strdup (action_keybinding); + else + info->keybinding = NULL; + info->do_action_func = action_func; + + cell->action_list = g_list_append (cell->action_list, (gpointer) info); + return TRUE; +} + +gboolean +gal_a11y_e_cell_remove_action (GalA11yECell *cell, + gint action_index) +{ + GList *list_node; + + g_return_val_if_fail (GAL_A11Y_IS_E_CELL (cell), FALSE); + list_node = g_list_nth (cell->action_list, action_index); + if (!list_node) + return FALSE; + g_return_val_if_fail (list_node->data != NULL, FALSE); + _gal_a11y_e_cell_destroy_action_info (list_node->data, NULL); + cell->action_list = g_list_remove_link (cell->action_list, list_node); + + return TRUE; +} + +gboolean +gal_a11y_e_cell_remove_action_by_name (GalA11yECell *cell, + const gchar *action_name) +{ + GList *list_node; + + g_return_val_if_fail (GAL_A11Y_IS_E_CELL (cell), FALSE); + + for (list_node = cell->action_list; list_node; list_node = list_node->next) { + if (!g_ascii_strcasecmp (((ActionInfo *)(list_node->data))->name, action_name)) { + break; + } + } + + g_return_val_if_fail (list_node != NULL, FALSE); + + _gal_a11y_e_cell_destroy_action_info (list_node->data, NULL); + cell->action_list = g_list_remove_link (cell->action_list, list_node); + + return TRUE; +} + +static gint +gal_a11y_e_cell_action_get_n_actions (AtkAction *action) +{ + GalA11yECell *cell = GAL_A11Y_E_CELL (action); + if (cell->action_list != NULL) + return g_list_length (cell->action_list); + else + return 0; +} + +static const gchar * +gal_a11y_e_cell_action_get_name (AtkAction *action, + gint index) +{ + GalA11yECell *cell = GAL_A11Y_E_CELL (action); + ActionInfo *info = _gal_a11y_e_cell_get_action_info (cell, index); + + if (info == NULL) + return NULL; + return info->name; +} + +static const gchar * +gal_a11y_e_cell_action_get_description (AtkAction *action, + gint index) +{ + GalA11yECell *cell = GAL_A11Y_E_CELL (action); + ActionInfo *info = _gal_a11y_e_cell_get_action_info (cell, index); + + if (info == NULL) + return NULL; + return info->description; +} + +static gboolean +gal_a11y_e_cell_action_set_description (AtkAction *action, + gint index, + const gchar *desc) +{ + GalA11yECell *cell = GAL_A11Y_E_CELL (action); + ActionInfo *info = _gal_a11y_e_cell_get_action_info (cell, index); + + if (info == NULL) + return FALSE; + g_free (info->description); + info->description = g_strdup (desc); + return TRUE; +} + +static const gchar * +gal_a11y_e_cell_action_get_keybinding (AtkAction *action, + gint index) +{ + GalA11yECell *cell = GAL_A11Y_E_CELL (action); + ActionInfo *info = _gal_a11y_e_cell_get_action_info (cell, index); + if (info == NULL) + return NULL; + + return info->keybinding; +} + +static gboolean +idle_do_action (gpointer data) +{ + GalA11yECell *cell; + + cell = GAL_A11Y_E_CELL (data); + + if (!is_valid (ATK_OBJECT (cell))) + return FALSE; + + cell->action_idle_handler = 0; + cell->action_func (cell); + g_object_unref (cell); + + return FALSE; +} + +static gboolean +gal_a11y_e_cell_action_do_action (AtkAction *action, + gint index) +{ + GalA11yECell *cell = GAL_A11Y_E_CELL (action); + ActionInfo *info = _gal_a11y_e_cell_get_action_info (cell, index); + + if (!is_valid (ATK_OBJECT (action))) + return FALSE; + + if (info == NULL) + return FALSE; + g_return_val_if_fail (info->do_action_func, FALSE); + if (cell->action_idle_handler) + return FALSE; + cell->action_func = info->do_action_func; + g_object_ref (cell); + cell->action_idle_handler = g_idle_add (idle_do_action, cell); + + return TRUE; +} + +static void +gal_a11y_e_cell_atk_action_interface_init (AtkActionIface *iface) +{ + g_return_if_fail (iface != NULL); + + iface->get_n_actions = gal_a11y_e_cell_action_get_n_actions; + iface->do_action = gal_a11y_e_cell_action_do_action; + iface->get_name = gal_a11y_e_cell_action_get_name; + iface->get_description = gal_a11y_e_cell_action_get_description; + iface->set_description = gal_a11y_e_cell_action_set_description; + iface->get_keybinding = gal_a11y_e_cell_action_get_keybinding; +} + +void +gal_a11y_e_cell_type_add_action_interface (GType type) +{ + static const GInterfaceInfo atk_action_info = + { + (GInterfaceInitFunc) gal_a11y_e_cell_atk_action_interface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + g_type_add_interface_static ( + type, ATK_TYPE_ACTION, + &atk_action_info); +} + +gboolean +gal_a11y_e_cell_add_state (GalA11yECell *cell, + AtkStateType state_type, + gboolean emit_signal) +{ + if (!atk_state_set_contains_state (cell->state_set, state_type)) { + gboolean rc; + + rc = atk_state_set_add_state (cell->state_set, state_type); + /* + * The signal should only be generated if the value changed, + * not when the cell is set up. So states that are set + * initially should pass FALSE as the emit_signal argument. + */ + + if (emit_signal) { + atk_object_notify_state_change (ATK_OBJECT (cell), state_type, TRUE); + /* If state_type is ATK_STATE_VISIBLE, additional + * notification */ + if (state_type == ATK_STATE_VISIBLE) + g_signal_emit_by_name (cell, "visible_data_changed"); + } + + return rc; + } + else + return FALSE; +} + +gboolean +gal_a11y_e_cell_remove_state (GalA11yECell *cell, + AtkStateType state_type, + gboolean emit_signal) +{ + if (atk_state_set_contains_state (cell->state_set, state_type)) { + gboolean rc; + + rc = atk_state_set_remove_state (cell->state_set, state_type); + /* + * The signal should only be generated if the value changed, + * not when the cell is set up. So states that are set + * initially should pass FALSE as the emit_signal argument. + */ + + if (emit_signal) { + atk_object_notify_state_change (ATK_OBJECT (cell), state_type, FALSE); + /* If state_type is ATK_STATE_VISIBLE, additional notification */ + if (state_type == ATK_STATE_VISIBLE) + g_signal_emit_by_name (cell, "visible_data_changed"); + } + + return rc; + } + else + return FALSE; +} + +/** + * gal_a11y_e_cell_get_type: + * @void: + * + * Registers the &GalA11yECell class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yECell class. + **/ +GType +gal_a11y_e_cell_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yECellClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) gal_a11y_e_cell_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yECell), + 0, + (GInstanceInitFunc) gal_a11y_e_cell_init, + NULL /* value_cell */ + }; + + static const GInterfaceInfo atk_component_info = { + (GInterfaceInitFunc) gal_a11y_e_cell_atk_component_iface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + type = g_type_register_static (PARENT_TYPE, "GalA11yECell", &info, 0); + g_type_add_interface_static (type, ATK_TYPE_COMPONENT, &atk_component_info); + } + + return type; +} + +AtkObject * +gal_a11y_e_cell_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row) +{ + AtkObject *a11y; + + a11y = g_object_new (gal_a11y_e_cell_get_type (), NULL); + + gal_a11y_e_cell_construct ( + a11y, + item, + cell_view, + parent, + model_col, + view_col, + row); + return a11y; +} + +void +gal_a11y_e_cell_construct (AtkObject *object, + ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row) +{ + GalA11yECell *a11y = GAL_A11Y_E_CELL (object); + a11y->item = item; + a11y->cell_view = cell_view; + a11y->parent = parent; + a11y->model_col = model_col; + a11y->view_col = view_col; + a11y->row = row; + ATK_OBJECT (a11y) ->role = ATK_ROLE_TABLE_CELL; + + if (item) + g_object_ref (item); + +#if 0 + if (parent) + g_object_ref (parent); + + if (cell_view) + g_object_ref (cell_view); + +#endif +} diff --git a/e-util/gal-a11y-e-cell.h b/e-util/gal-a11y-e-cell.h new file mode 100644 index 0000000000..63e8ecfe6b --- /dev/null +++ b/e-util/gal-a11y-e-cell.h @@ -0,0 +1,112 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_CELL_H__ +#define __GAL_A11Y_E_CELL_H__ + +#include <e-util/e-table-item.h> +#include <e-util/e-cell.h> + +#define GAL_A11Y_TYPE_E_CELL (gal_a11y_e_cell_get_type ()) +#define GAL_A11Y_E_CELL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_CELL, GalA11yECell)) +#define GAL_A11Y_E_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_CELL, GalA11yECellClass)) +#define GAL_A11Y_IS_E_CELL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_CELL)) +#define GAL_A11Y_IS_E_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_CELL)) + +typedef struct _GalA11yECell GalA11yECell; +typedef struct _GalA11yECellClass GalA11yECellClass; +typedef struct _GalA11yECellPrivate GalA11yECellPrivate; +typedef struct _ActionInfo ActionInfo; +typedef void (*ACTION_FUNC) (GalA11yECell *cell); + +/* This struct should actually be larger as this isn't what we derive from. + * The GalA11yECellPrivate comes right after the parent class structure. + **/ +struct _GalA11yECell { + AtkObject object; + + ETableItem *item; + ECellView *cell_view; + AtkObject *parent; + gint model_col; + gint view_col; + gint row; + AtkStateSet *state_set; + GList *action_list; + gint action_idle_handler; + ACTION_FUNC action_func; +}; + +struct _GalA11yECellClass { + AtkObjectClass parent_class; +}; + +struct _ActionInfo { + gchar *name; + gchar *description; + gchar *keybinding; + ACTION_FUNC do_action_func; +}; + +/* Standard Glib function */ +GType gal_a11y_e_cell_get_type (void); +AtkObject *gal_a11y_e_cell_new (ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row); +void gal_a11y_e_cell_construct (AtkObject *object, + ETableItem *item, + ECellView *cell_view, + AtkObject *parent, + gint model_col, + gint view_col, + gint row); + +void gal_a11y_e_cell_type_add_action_interface (GType type); + +gboolean gal_a11y_e_cell_add_action (GalA11yECell *cell, + const gchar *action_name, + const gchar *action_description, + const gchar *action_keybinding, + ACTION_FUNC action_func); + +gboolean gal_a11y_e_cell_remove_action (GalA11yECell *cell, + gint action_id); + +gboolean gal_a11y_e_cell_remove_action_by_name (GalA11yECell *cell, + const gchar *action_name); + +gboolean gal_a11y_e_cell_add_state (GalA11yECell *cell, + AtkStateType state_type, + gboolean emit_signal); + +gboolean gal_a11y_e_cell_remove_state (GalA11yECell *cell, + AtkStateType state_type, + gboolean emit_signal); + +#endif /* __GAL_A11Y_E_CELL_H__ */ diff --git a/e-util/gal-a11y-e-table-click-to-add-factory.c b/e-util/gal-a11y-e-table-click-to-add-factory.c new file mode 100644 index 0000000000..ff923d8e40 --- /dev/null +++ b/e-util/gal-a11y-e-table-click-to-add-factory.c @@ -0,0 +1,108 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yuedong Du <yuedong.du@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-table-click-to-add-factory.h" + +#include <atk/atk.h> + +#include "e-table-click-to-add.h" +#include "e-table.h" +#include "gal-a11y-e-table-click-to-add.h" +#include "gal-a11y-e-table.h" + +#define CS_CLASS(factory) (G_TYPE_INSTANCE_GET_CLASS ((factory), C_TYPE_STREAM, GalA11yETableClickToAddFactoryClass)) +static AtkObjectFactoryClass *parent_class; +#define PARENT_TYPE (ATK_TYPE_OBJECT_FACTORY) + +/* Static functions */ + +static GType +gal_a11y_e_table_click_to_add_factory_get_accessible_type (void) +{ + return GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD; +} + +static AtkObject * +gal_a11y_e_table_click_to_add_factory_create_accessible (GObject *obj) +{ + AtkObject * atk_object; + + g_return_val_if_fail (E_IS_TABLE_CLICK_TO_ADD (obj), NULL); + + atk_object = gal_a11y_e_table_click_to_add_new (obj); + + return atk_object; +} + +static void +gal_a11y_e_table_click_to_add_factory_class_init (GalA11yETableClickToAddFactoryClass *class) +{ + AtkObjectFactoryClass *factory_class = ATK_OBJECT_FACTORY_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + factory_class->create_accessible = gal_a11y_e_table_click_to_add_factory_create_accessible; + factory_class->get_accessible_type = gal_a11y_e_table_click_to_add_factory_get_accessible_type; +} + +static void +gal_a11y_e_table_click_to_add_factory_init (GalA11yETableClickToAddFactory *factory) +{ +} + +/** + * gal_a11y_e_table_factory_get_type: + * @void: + * + * Registers the &GalA11yETableFactory class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yETableFactory class. + **/ +GType +gal_a11y_e_table_click_to_add_factory_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yETableClickToAddFactoryClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) gal_a11y_e_table_click_to_add_factory_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yETableClickToAddFactory), + 0, + (GInstanceInitFunc) gal_a11y_e_table_click_to_add_factory_init, + NULL /* value_table */ + }; + + type = g_type_register_static (PARENT_TYPE, "GalA11yETableClickToAddFactory", &info, 0); + } + + return type; +} diff --git a/e-util/gal-a11y-e-table-click-to-add-factory.h b/e-util/gal-a11y-e-table-click-to-add-factory.h new file mode 100644 index 0000000000..cc6d47f6b1 --- /dev/null +++ b/e-util/gal-a11y-e-table-click-to-add-factory.h @@ -0,0 +1,52 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yuedong Du <yuedong.du@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TABLE_CLICK_TO_ADD_FACTORY_H__ +#define __GAL_A11Y_E_TABLE_CLICK_TO_ADD_FACTORY_H__ + +#include <atk/atkobjectfactory.h> + +#define GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD_FACTORY (gal_a11y_e_table_item_click_to_add_factory_get_type ()) +#define GAL_A11Y_E_TABLE_CLICK_TO_ADD_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD_FACTORY, GalA11yETableClickToAddFactory)) +#define GAL_A11Y_E_TABLE_CLICK_TO_ADD_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD_FACTORY, GalA11yETableClickToAddFactoryClass)) +#define GAL_A11Y_IS_E_TABLE_CLICK_TO_ADD_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD_FACTORY)) +#define GAL_A11Y_IS_E_TABLE_CLICK_TO_ADD_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD_FACTORY)) + +typedef struct _GalA11yETableClickToAddFactory GalA11yETableClickToAddFactory; +typedef struct _GalA11yETableClickToAddFactoryClass GalA11yETableClickToAddFactoryClass; + +struct _GalA11yETableClickToAddFactory { + AtkObject object; +}; + +struct _GalA11yETableClickToAddFactoryClass { + AtkObjectClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_table_click_to_add_factory_get_type (void); + +#endif diff --git a/e-util/gal-a11y-e-table-click-to-add.c b/e-util/gal-a11y-e-table-click-to-add.c new file mode 100644 index 0000000000..bebe8c44a9 --- /dev/null +++ b/e-util/gal-a11y-e-table-click-to-add.c @@ -0,0 +1,358 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yuedong Du <yuedong.du@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-table-click-to-add.h" + +#include <atk/atk.h> +#include <glib/gi18n.h> + +#include "e-table-click-to-add.h" +#include "e-table-group-leaf.h" +#include "e-table-group.h" +#include "gal-a11y-e-table-click-to-add-factory.h" +#include "gal-a11y-util.h" + +static AtkObjectClass *parent_class; +static GType parent_type; +static gint priv_offset; +#define GET_PRIVATE(object) \ + ((GalA11yETableClickToAddPrivate *) \ + (((gchar *) object) + priv_offset)) +#define PARENT_TYPE (parent_type) + +struct _GalA11yETableClickToAddPrivate { + gpointer rect; + gpointer row; +}; + +static gint +etcta_get_n_actions (AtkAction *action) +{ + return 1; +} + +static const gchar * +etcta_get_description (AtkAction *action, + gint i) +{ + if (i == 0) + return _("click to add"); + + return NULL; +} + +static const gchar * +etcta_action_get_name (AtkAction *action, + gint i) +{ + if (i == 0) + return _("click"); + + return NULL; +} + +static gboolean +idle_do_action (gpointer data) +{ + GtkLayout *layout; + GdkEventButton event; + ETableClickToAdd * etcta; + gint finished; + + g_return_val_if_fail (data!= NULL, FALSE); + + etcta = E_TABLE_CLICK_TO_ADD ( + atk_gobject_accessible_get_object ( + ATK_GOBJECT_ACCESSIBLE (data))); + g_return_val_if_fail (etcta, FALSE); + + layout = GTK_LAYOUT (GNOME_CANVAS_ITEM (etcta)->canvas); + + event.x = 0; + event.y = 0; + event.type = GDK_BUTTON_PRESS; + event.window = gtk_layout_get_bin_window (layout); + event.button = 1; + event.send_event = TRUE; + event.time = GDK_CURRENT_TIME; + event.axes = NULL; + + g_signal_emit_by_name (etcta, "event", &event, &finished); + + return FALSE; +} + +static gboolean +etcta_do_action (AtkAction *action, + gint i) +{ + g_return_val_if_fail (i == 0, FALSE); + + g_idle_add (idle_do_action, action); + + return TRUE; +} + +static void +atk_action_interface_init (AtkActionIface *iface) +{ + g_return_if_fail (iface != NULL); + + iface->do_action = etcta_do_action; + iface->get_n_actions = etcta_get_n_actions; + iface->get_description = etcta_get_description; + iface->get_name = etcta_action_get_name; +} + +static const gchar * +etcta_get_name (AtkObject *obj) +{ + ETableClickToAdd * etcta; + + g_return_val_if_fail (GAL_A11Y_IS_E_TABLE_CLICK_TO_ADD (obj), NULL); + + etcta = E_TABLE_CLICK_TO_ADD ( + atk_gobject_accessible_get_object ( + ATK_GOBJECT_ACCESSIBLE (obj))); + if (etcta && etcta->message != NULL) + return etcta->message; + + return _("click to add"); +} + +static gint +etcta_get_n_children (AtkObject *accessible) +{ + return 1; +} + +static AtkObject * +etcta_ref_child (AtkObject *accessible, + gint i) +{ + AtkObject * atk_obj = NULL; + ETableClickToAdd * etcta; + + if (i != 0) + return NULL; + + etcta = E_TABLE_CLICK_TO_ADD ( + atk_gobject_accessible_get_object ( + ATK_GOBJECT_ACCESSIBLE (accessible))); + + g_return_val_if_fail (etcta, NULL); + + if (etcta->rect) { + atk_obj = atk_gobject_accessible_for_object ( + G_OBJECT (etcta->rect)); + } else if (etcta->row) { + atk_obj = atk_gobject_accessible_for_object ( + G_OBJECT (etcta->row)); + } + + g_object_ref (atk_obj); + + return atk_obj; +} + +static AtkStateSet * +etcta_ref_state_set (AtkObject *accessible) +{ + AtkStateSet * state_set = NULL; + + state_set = ATK_OBJECT_CLASS (parent_class)->ref_state_set (accessible); + if (state_set != NULL) { + atk_state_set_add_state (state_set, ATK_STATE_SENSITIVE); + atk_state_set_add_state (state_set, ATK_STATE_SHOWING); + } + + return state_set; +} + +static void +etcta_class_init (GalA11yETableClickToAddClass *class) +{ + AtkObjectClass *atk_object_class = ATK_OBJECT_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + atk_object_class->get_name = etcta_get_name; + atk_object_class->get_n_children = etcta_get_n_children; + atk_object_class->ref_child = etcta_ref_child; + atk_object_class->ref_state_set = etcta_ref_state_set; +} + +static void +etcta_init (GalA11yETableClickToAdd *a11y) +{ +} + +GType +gal_a11y_e_table_click_to_add_get_type (void) +{ + static GType type = 0; + + if (!type) { + AtkObjectFactory *factory; + + GTypeInfo info = { + sizeof (GalA11yETableClickToAddClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) etcta_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yETableClickToAdd), + 0, + (GInstanceInitFunc) etcta_init, + NULL /* value_table */ + }; + + static const GInterfaceInfo atk_action_info = { + (GInterfaceInitFunc) atk_action_interface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + factory = atk_registry_get_factory ( + atk_get_default_registry (), + GNOME_TYPE_CANVAS_ITEM); + + parent_type = atk_object_factory_get_accessible_type (factory); + type = gal_a11y_type_register_static_with_private ( + PARENT_TYPE, "GalA11yETableClickToAdd", &info, 0, + sizeof (GalA11yETableClickToAddPrivate), &priv_offset); + + g_type_add_interface_static (type, ATK_TYPE_ACTION, &atk_action_info); + + } + + return type; +} + +static gboolean +etcta_event (GnomeCanvasItem *item, + GdkEvent *e, + gpointer data) +{ + ETableClickToAdd *etcta = E_TABLE_CLICK_TO_ADD (item); + GalA11yETableClickToAdd *a11y; + GalA11yETableClickToAddPrivate *priv; + + g_return_val_if_fail (item, TRUE); + + g_return_val_if_fail (GAL_A11Y_IS_E_TABLE_CLICK_TO_ADD (data), FALSE); + a11y = GAL_A11Y_E_TABLE_CLICK_TO_ADD (data); + + priv = GET_PRIVATE (a11y); + + /* rect replaced by row. */ + if (etcta->rect == NULL && priv->rect != NULL) { + g_signal_emit_by_name (a11y, "children_changed::remove", 0, NULL, NULL); + + } + /* row inserted, and/or replaced by a new row. */ + if (etcta->row != NULL && priv->row == NULL) { + g_signal_emit_by_name (a11y, "children_changed::add", 0, NULL, NULL); + } else if (etcta->row != NULL && priv->row != NULL && etcta->row != priv->row) { + g_signal_emit_by_name (a11y, "children_changed::remove", 0, NULL, NULL); + g_signal_emit_by_name (a11y, "children_changed::add", 0, NULL, NULL); + } + + priv->rect = etcta->rect; + priv->row = etcta->row; + + return FALSE; +} + +static void +etcta_selection_cursor_changed (ESelectionModel *esm, + gint row, + gint col, + GalA11yETableClickToAdd *a11y) +{ + ETableClickToAdd *etcta; + AtkObject *row_a11y; + + etcta = E_TABLE_CLICK_TO_ADD ( + atk_gobject_accessible_get_object ( + ATK_GOBJECT_ACCESSIBLE (a11y))); + + if (etcta == NULL || etcta->row == NULL) + return; + + row_a11y = atk_gobject_accessible_for_object (G_OBJECT (etcta->row)); + if (row_a11y) { + AtkObject *cell_a11y; + + cell_a11y = g_object_get_data ( + G_OBJECT (row_a11y), "gail-focus-object"); + if (cell_a11y) { + atk_focus_tracker_notify (cell_a11y); + } + } +} + +AtkObject * +gal_a11y_e_table_click_to_add_new (GObject *widget) +{ + GalA11yETableClickToAdd *a11y; + ETableClickToAdd * etcta; + GalA11yETableClickToAddPrivate *priv; + + g_return_val_if_fail (widget != NULL, NULL); + + a11y = g_object_new (gal_a11y_e_table_click_to_add_get_type (), NULL); + priv = GET_PRIVATE (a11y); + + etcta = E_TABLE_CLICK_TO_ADD (widget); + + atk_object_initialize (ATK_OBJECT (a11y), etcta); + + priv->rect = etcta->rect; + priv->row = etcta->row; + + g_signal_connect_after ( + widget, "event", + G_CALLBACK (etcta_event), a11y); + + g_signal_connect ( + etcta->selection, "cursor_changed", + G_CALLBACK (etcta_selection_cursor_changed), a11y); + + return ATK_OBJECT (a11y); +} + +void +gal_a11y_e_table_click_to_add_init (void) +{ + if (atk_get_root ()) + atk_registry_set_factory_type ( + atk_get_default_registry (), + E_TYPE_TABLE_CLICK_TO_ADD, + gal_a11y_e_table_click_to_add_factory_get_type ()); +} + diff --git a/e-util/gal-a11y-e-table-click-to-add.h b/e-util/gal-a11y-e-table-click-to-add.h new file mode 100644 index 0000000000..46f3939bbe --- /dev/null +++ b/e-util/gal-a11y-e-table-click-to-add.h @@ -0,0 +1,58 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TABLE_CLICK_TO_ADD_H__ +#define __GAL_A11Y_E_TABLE_CLICK_TO_ADD_H__ + +#include <atk/atkgobjectaccessible.h> +#include <e-util/e-table-item.h> + +#define GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD (gal_a11y_e_table_click_to_add_get_type ()) +#define GAL_A11Y_E_TABLE_CLICK_TO_ADD(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD, GalA11yETableClickToAdd)) +#define GAL_A11Y_E_TABLE_CLICK_TO_ADD_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD, GalA11yETableClickToAddClass)) +#define GAL_A11Y_IS_E_TABLE_CLICK_TO_ADD(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD)) +#define GAL_A11Y_IS_E_TABLE_CLICK_TO_ADD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TABLE_CLICK_TO_ADD)) + +typedef struct _GalA11yETableClickToAdd GalA11yETableClickToAdd; +typedef struct _GalA11yETableClickToAddClass GalA11yETableClickToAddClass; +typedef struct _GalA11yETableClickToAddPrivate GalA11yETableClickToAddPrivate; + +/* This struct should actually be larger as this isn't what we derive from. + * The GalA11yETableClickToAddPrivate comes right after the parent class structure. + **/ +struct _GalA11yETableClickToAdd { + AtkGObjectAccessible parent; +}; + +struct _GalA11yETableClickToAddClass { + AtkGObjectAccessibleClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_table_click_to_add_get_type (void); +AtkObject *gal_a11y_e_table_click_to_add_new (GObject *widget); + +void gal_a11y_e_table_click_to_add_init (void); +#endif /* __GAL_A11Y_E_TABLE_CLICK_TO_ADD_H__ */ diff --git a/e-util/gal-a11y-e-table-column-header.c b/e-util/gal-a11y-e-table-column-header.c new file mode 100644 index 0000000000..46fb374e9a --- /dev/null +++ b/e-util/gal-a11y-e-table-column-header.c @@ -0,0 +1,243 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Li Yuan <li.yuan@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-table-column-header.h" + +#include <glib/gi18n.h> +#include <atk/atkobject.h> +#include <atk/atkregistry.h> + +#include "e-table-header-item.h" +#include "gal-a11y-util.h" + +static GObjectClass *parent_class; +static gint priv_offset; + +#define GET_PRIVATE(object) \ + ((GalA11yETableColumnHeaderPrivate *) \ + (((gchar *) object) + priv_offset)) +#define PARENT_TYPE (atk_gobject_accessible_get_type ()) + +struct _GalA11yETableColumnHeaderPrivate { + ETableItem *item; + AtkObject *parent; + AtkStateSet *state_set; +}; + +static void +etch_init (GalA11yETableColumnHeader *a11y) +{ + GET_PRIVATE (a11y)->item = NULL; + GET_PRIVATE (a11y)->parent = NULL; + GET_PRIVATE (a11y)->state_set = NULL; +} + +static AtkStateSet * +gal_a11y_e_table_column_header_ref_state_set (AtkObject *accessible) +{ + GalA11yETableColumnHeaderPrivate *priv = GET_PRIVATE (accessible); + + g_return_val_if_fail (priv->state_set, NULL); + + g_object_ref (priv->state_set); + + return priv->state_set; +} + +static void +gal_a11y_e_table_column_header_real_initialize (AtkObject *obj, + gpointer data) +{ + ATK_OBJECT_CLASS (parent_class)->initialize (obj, data); +} + +static void +gal_a11y_e_table_column_header_dispose (GObject *object) +{ + GalA11yETableColumnHeader *a11y = GAL_A11Y_E_TABLE_COLUMN_HEADER (object); + GalA11yETableColumnHeaderPrivate *priv = GET_PRIVATE (a11y); + + if (priv->state_set) { + g_object_unref (priv->state_set); + priv->state_set = NULL; + } + + if (parent_class->dispose) + parent_class->dispose (object); + +} + +static void +etch_class_init (GalA11yETableColumnHeaderClass *class) +{ + AtkObjectClass *atk_object_class = ATK_OBJECT_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + object_class->dispose = gal_a11y_e_table_column_header_dispose; + + atk_object_class->ref_state_set = gal_a11y_e_table_column_header_ref_state_set; + atk_object_class->initialize = gal_a11y_e_table_column_header_real_initialize; +} + +inline static GObject * +etch_a11y_get_gobject (AtkGObjectAccessible *accessible) +{ + return atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (accessible)); +} + +static gboolean +gal_a11y_e_table_column_header_do_action (AtkAction *action, + gint i) +{ + gboolean return_value = TRUE; + GtkWidget *widget; + GalA11yETableColumnHeader *a11y; + ETableHeaderItem *ethi; + ETableItem *item; + ETableCol *col; + + switch (i) { + case 0: + a11y = GAL_A11Y_E_TABLE_COLUMN_HEADER (action); + col = E_TABLE_COL (etch_a11y_get_gobject ( + ATK_GOBJECT_ACCESSIBLE (a11y))); + item = GET_PRIVATE (a11y)->item; + widget = gtk_widget_get_parent (GTK_WIDGET (item->parent.canvas)); + if (E_IS_TREE (widget)) { + ethi = E_TABLE_HEADER_ITEM ( + e_tree_get_header_item (E_TREE (widget))); + } + else if (E_IS_TABLE (widget)) + ethi = E_TABLE_HEADER_ITEM ( + E_TABLE (widget)->header_item); + else + break; + ethi_change_sort_state (ethi, col); + default: + return_value = FALSE; + break; + } + return return_value; +} + +static gint +gal_a11y_e_table_column_header_get_n_actions (AtkAction *action) +{ + return 1; +} + +static const gchar * +gal_a11y_e_table_column_header_action_get_name (AtkAction *action, + gint i) +{ + const gchar *return_value; + + switch (i) { + case 0: + return_value = _("sort"); + break; + default: + return_value = NULL; + break; + } + return return_value; +} + +static void +atk_action_interface_init (AtkActionIface *iface) +{ + g_return_if_fail (iface != NULL); + + iface->do_action = gal_a11y_e_table_column_header_do_action; + iface->get_n_actions = gal_a11y_e_table_column_header_get_n_actions; + iface->get_name = gal_a11y_e_table_column_header_action_get_name; +} + +GType +gal_a11y_e_table_column_header_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yETableColumnHeaderClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) etch_class_init, + (GClassFinalizeFunc) NULL, + NULL, + sizeof (GalA11yETableColumnHeader), + 0, + (GInstanceInitFunc) etch_init, + NULL + }; + static const GInterfaceInfo atk_action_info = { + (GInterfaceInitFunc) atk_action_interface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + type = gal_a11y_type_register_static_with_private ( + PARENT_TYPE, "GalA11yETableColumnHeader", &info, 0, + sizeof (GalA11yETableColumnHeaderPrivate), &priv_offset); + + g_type_add_interface_static ( + type, ATK_TYPE_ACTION, &atk_action_info); + } + + return type; +} + +AtkObject * +gal_a11y_e_table_column_header_new (ETableCol *ecol, + ETableItem *item) +{ + GalA11yETableColumnHeader *a11y; + AtkObject *accessible; + + g_return_val_if_fail (E_IS_TABLE_COL (ecol), NULL); + + a11y = g_object_new (gal_a11y_e_table_column_header_get_type (), NULL); + accessible = ATK_OBJECT (a11y); + atk_object_initialize (accessible, ecol); + + GET_PRIVATE (a11y)->item = item; + GET_PRIVATE (a11y)->state_set = atk_state_set_new (); + + atk_state_set_add_state (GET_PRIVATE (a11y)->state_set, ATK_STATE_VISIBLE); + atk_state_set_add_state (GET_PRIVATE (a11y)->state_set, ATK_STATE_SHOWING); + atk_state_set_add_state (GET_PRIVATE (a11y)->state_set, ATK_STATE_SENSITIVE); + atk_state_set_add_state (GET_PRIVATE (a11y)->state_set, ATK_STATE_ENABLED); + + if (ecol->text) + atk_object_set_name (accessible, ecol->text); + atk_object_set_role (accessible, ATK_ROLE_TABLE_COLUMN_HEADER); + + return ATK_OBJECT (a11y); +} diff --git a/e-util/gal-a11y-e-table-column-header.h b/e-util/gal-a11y-e-table-column-header.h new file mode 100644 index 0000000000..9d77467bda --- /dev/null +++ b/e-util/gal-a11y-e-table-column-header.h @@ -0,0 +1,59 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Li Yuan <li.yuan@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TABLE_COLUMN_HEADER_H__ +#define __GAL_A11Y_E_TABLE_COLUMN_HEADER_H__ + +#include <atk/atkgobjectaccessible.h> + +#include <e-util/e-table-col.h> +#include <e-util/e-table-item.h> + +#define GAL_A11Y_TYPE_E_TABLE_COLUMN_HEADER (gal_a11y_e_table_column_header_get_type ()) +#define GAL_A11Y_E_TABLE_COLUMN_HEADER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TABLE_COLUMN_HEADER, GalA11yETableColumnHeader)) +#define GAL_A11Y_E_TABLE_COLUMN_HEADER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TABLE_COLUMN_HEADER, GalA11yETableColumnHeaderClass)) +#define GAL_A11Y_IS_E_TABLE_COLUMN_HEADER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TABLE_COLUMN_HEADER)) +#define GAL_A11Y_IS_E_TABLE_COLUMN_HEADER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TABLE_COLUMN_HEADER)) + +typedef struct _GalA11yETableColumnHeader GalA11yETableColumnHeader; +typedef struct _GalA11yETableColumnHeaderClass GalA11yETableColumnHeaderClass; +typedef struct _GalA11yETableColumnHeaderPrivate GalA11yETableColumnHeaderPrivate; + +struct _GalA11yETableColumnHeader { + AtkGObjectAccessible parent; +}; + +struct _GalA11yETableColumnHeaderClass { + AtkGObjectAccessibleClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_table_column_header_get_type (void); +AtkObject *gal_a11y_e_table_column_header_new (ETableCol *etc, ETableItem *item); +void gal_a11y_e_table_column_header_init (void); + +#endif /* __GAL_A11Y_E_TABLE_COLUMN_HEADER_H__ */ diff --git a/e-util/gal-a11y-e-table-factory.c b/e-util/gal-a11y-e-table-factory.c new file mode 100644 index 0000000000..a3905ab393 --- /dev/null +++ b/e-util/gal-a11y-e-table-factory.c @@ -0,0 +1,101 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-table.h" +#include "gal-a11y-e-table-factory.h" + +static AtkObjectFactoryClass *parent_class; +#define PARENT_TYPE (ATK_TYPE_OBJECT_FACTORY) + +/* Static functions */ + +static GType +gal_a11y_e_table_factory_get_accessible_type (void) +{ + return GAL_A11Y_TYPE_E_TABLE; +} + +static AtkObject * +gal_a11y_e_table_factory_create_accessible (GObject *obj) +{ + AtkObject *accessible; + + accessible = gal_a11y_e_table_new (obj); + + return accessible; +} + +static void +gal_a11y_e_table_factory_class_init (GalA11yETableFactoryClass *class) +{ + AtkObjectFactoryClass *factory_class = ATK_OBJECT_FACTORY_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + factory_class->create_accessible = gal_a11y_e_table_factory_create_accessible; + factory_class->get_accessible_type = gal_a11y_e_table_factory_get_accessible_type; +} + +static void +gal_a11y_e_table_factory_init (GalA11yETableFactory *factory) +{ +} + +/** + * gal_a11y_e_table_factory_get_type: + * @void: + * + * Registers the &GalA11yETableFactory class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yETableFactory class. + **/ +GType +gal_a11y_e_table_factory_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yETableFactoryClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) gal_a11y_e_table_factory_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yETableFactory), + 0, + (GInstanceInitFunc) gal_a11y_e_table_factory_init, + NULL /* value_table */ + }; + + type = g_type_register_static ( + PARENT_TYPE, "GalA11yETableFactory", &info, 0); + } + + return type; +} diff --git a/e-util/gal-a11y-e-table-factory.h b/e-util/gal-a11y-e-table-factory.h new file mode 100644 index 0000000000..3a8b18f32f --- /dev/null +++ b/e-util/gal-a11y-e-table-factory.h @@ -0,0 +1,53 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TABLE_FACTORY_H__ +#define __GAL_A11Y_E_TABLE_FACTORY_H__ + +#include <atk/atkobjectfactory.h> + +#define GAL_A11Y_TYPE_E_TABLE_FACTORY (gal_a11y_e_table_factory_get_type ()) +#define GAL_A11Y_E_TABLE_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TABLE_FACTORY, GalA11yETableFactory)) +#define GAL_A11Y_E_TABLE_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TABLE_FACTORY, GalA11yETableFactoryClass)) +#define GAL_A11Y_IS_E_TABLE_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TABLE_FACTORY)) +#define GAL_A11Y_IS_E_TABLE_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TABLE_FACTORY)) + +typedef struct _GalA11yETableFactory GalA11yETableFactory; +typedef struct _GalA11yETableFactoryClass GalA11yETableFactoryClass; + +struct _GalA11yETableFactory { + AtkObject object; +}; + +struct _GalA11yETableFactoryClass { + AtkObjectClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_table_factory_get_type (void); + +#endif /* __GAL_A11Y_E_TABLE_FACTORY_H__ */ diff --git a/e-util/gal-a11y-e-table-item-factory.c b/e-util/gal-a11y-e-table-item-factory.c new file mode 100644 index 0000000000..3ef551d66a --- /dev/null +++ b/e-util/gal-a11y-e-table-item-factory.c @@ -0,0 +1,107 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yuedong Du <yuedong.du@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-table-item-factory.h" + +#include <atk/atk.h> + +#include "e-table.h" +#include "e-tree.h" +#include "gal-a11y-e-table-item.h" +#include "gal-a11y-e-table.h" + +#define CS_CLASS(factory) (G_TYPE_INSTANCE_GET_CLASS ((factory), C_TYPE_STREAM, GalA11yETableItemFactoryClass)) +static AtkObjectFactoryClass *parent_class; +#define PARENT_TYPE (ATK_TYPE_OBJECT_FACTORY) + +/* Static functions */ + +static GType +gal_a11y_e_table_item_factory_get_accessible_type (void) +{ + return GAL_A11Y_TYPE_E_TABLE_ITEM; +} + +static AtkObject * +gal_a11y_e_table_item_factory_create_accessible (GObject *obj) +{ + AtkObject *accessible; + + g_return_val_if_fail (E_IS_TABLE_ITEM (obj), NULL); + accessible = gal_a11y_e_table_item_new (E_TABLE_ITEM (obj)); + + return accessible; +} + +static void +gal_a11y_e_table_item_factory_class_init (GalA11yETableItemFactoryClass *class) +{ + AtkObjectFactoryClass *factory_class = ATK_OBJECT_FACTORY_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + factory_class->create_accessible = gal_a11y_e_table_item_factory_create_accessible; + factory_class->get_accessible_type = gal_a11y_e_table_item_factory_get_accessible_type; +} + +static void +gal_a11y_e_table_item_factory_init (GalA11yETableItemFactory *factory) +{ +} + +/** + * gal_a11y_e_table_factory_get_type: + * @void: + * + * Registers the &GalA11yETableFactory class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yETableFactory class. + **/ +GType +gal_a11y_e_table_item_factory_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yETableItemFactoryClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) gal_a11y_e_table_item_factory_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yETableItemFactory), + 0, + (GInstanceInitFunc) gal_a11y_e_table_item_factory_init, + NULL /* value_table */ + }; + + type = g_type_register_static (PARENT_TYPE, "GalA11yETableItemFactory", &info, 0); + } + + return type; +} diff --git a/e-util/gal-a11y-e-table-item-factory.h b/e-util/gal-a11y-e-table-item-factory.h new file mode 100644 index 0000000000..4aef02d113 --- /dev/null +++ b/e-util/gal-a11y-e-table-item-factory.h @@ -0,0 +1,52 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yuedong Du <yuedong.du@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TABLE_ITEM_FACTORY_H__ +#define __GAL_A11Y_E_TABLE_ITEM_FACTORY_H__ + +#include <atk/atkobjectfactory.h> + +#define GAL_A11Y_TYPE_E_TABLE_ITEM_FACTORY (gal_a11y_e_table_item_factory_get_type ()) +#define GAL_A11Y_E_TABLE_ITEM_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TABLE_ITEM_FACTORY, GalA11yETableItemFactory)) +#define GAL_A11Y_E_TABLE_ITEM_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TABLE_ITEM_FACTORY, GalA11yETableItemFactoryClass)) +#define GAL_A11Y_IS_E_TABLE_ITEM_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TABLE_ITEM_FACTORY)) +#define GAL_A11Y_IS_E_TABLE_ITEM_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TABLE_ITEM_FACTORY)) + +typedef struct _GalA11yETableItemFactory GalA11yETableItemFactory; +typedef struct _GalA11yETableItemFactoryClass GalA11yETableItemFactoryClass; + +struct _GalA11yETableItemFactory { + AtkObject object; +}; + +struct _GalA11yETableItemFactoryClass { + AtkObjectClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_table_item_factory_get_type (void); + +#endif /* __GAL_A11Y_E_TABLE_FACTORY_H__ */ diff --git a/e-util/gal-a11y-e-table-item.c b/e-util/gal-a11y-e-table-item.c new file mode 100644 index 0000000000..9f5c407507 --- /dev/null +++ b/e-util/gal-a11y-e-table-item.c @@ -0,0 +1,1437 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * Bolian Yin <bolian.yin@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-table-item.h" + +#include <string.h> + +#include <atk/atk.h> + +#include "e-canvas.h" +#include "e-selection-model.h" +#include "e-table-click-to-add.h" +#include "e-table-subset.h" +#include "e-table.h" +#include "e-tree.h" +#include "gal-a11y-e-cell-registry.h" +#include "gal-a11y-e-cell.h" +#include "gal-a11y-e-table-click-to-add.h" +#include "gal-a11y-e-table-column-header.h" +#include "gal-a11y-e-table-item-factory.h" +#include "gal-a11y-util.h" + +static GObjectClass *parent_class; +static AtkComponentIface *component_parent_iface; +static GType parent_type; +static gint priv_offset; +static GQuark quark_accessible_object = 0; +#define GET_PRIVATE(object) \ + ((GalA11yETableItemPrivate *) (((gchar *) object) + priv_offset)) +#define PARENT_TYPE (parent_type) + +struct _GalA11yETableItemPrivate { + ETableItem *item; + gint cols; + gint rows; + gint selection_change_id; + gint cursor_change_id; + ETableCol ** columns; + ESelectionModel *selection; + AtkStateSet *state_set; + GtkWidget *widget; +}; + +static gboolean gal_a11y_e_table_item_ref_selection (GalA11yETableItem *a11y, + ESelectionModel *selection); +static gboolean gal_a11y_e_table_item_unref_selection (GalA11yETableItem *a11y); + +static AtkObject * eti_ref_at (AtkTable *table, gint row, gint column); + +static void +free_columns (ETableCol **columns) +{ + gint ii; + + if (!columns) + return; + + for (ii = 0; columns[ii]; ii++) { + g_object_unref (columns[ii]); + } + + g_free (columns); +} + +static void +item_finalized (gpointer user_data, + GObject *gone_item) +{ + GalA11yETableItem *a11y; + GalA11yETableItemPrivate *priv; + + a11y = GAL_A11Y_E_TABLE_ITEM (user_data); + priv = GET_PRIVATE (a11y); + + priv->item = NULL; + + atk_state_set_add_state (priv->state_set, ATK_STATE_DEFUNCT); + atk_object_notify_state_change (ATK_OBJECT (a11y), ATK_STATE_DEFUNCT, TRUE); + + if (priv->selection) + gal_a11y_e_table_item_unref_selection (a11y); + + g_object_unref (a11y); +} + +static AtkStateSet * +eti_ref_state_set (AtkObject *accessible) +{ + GalA11yETableItemPrivate *priv = GET_PRIVATE (accessible); + + g_object_ref (priv->state_set); + + return priv->state_set; +} + +inline static gint +view_to_model_row (ETableItem *eti, + gint row) +{ + if (eti->uses_source_model) { + ETableSubset *etss = E_TABLE_SUBSET (eti->table_model); + if (row >= 0 && row < etss->n_map) { + eti->row_guess = row; + return etss->map_table[row]; + } else + return -1; + } else + return row; +} + +inline static gint +view_to_model_col (ETableItem *eti, + gint col) +{ + ETableCol *ecol = e_table_header_get_column (eti->header, col); + return ecol ? ecol->col_idx : -1; +} + +inline static gint +model_to_view_row (ETableItem *eti, + gint row) +{ + gint i; + if (row == -1) + return -1; + if (eti->uses_source_model) { + ETableSubset *etss = E_TABLE_SUBSET (eti->table_model); + if (eti->row_guess >= 0 && eti->row_guess < etss->n_map) { + if (etss->map_table[eti->row_guess] == row) { + return eti->row_guess; + } + } + for (i = 0; i < etss->n_map; i++) { + if (etss->map_table[i] == row) + return i; + } + return -1; + } else + return row; +} + +inline static gint +model_to_view_col (ETableItem *eti, + gint col) +{ + gint i; + if (col == -1) + return -1; + for (i = 0; i < eti->cols; i++) { + ETableCol *ecol = e_table_header_get_column (eti->header, i); + if (ecol->col_idx == col) + return i; + } + return -1; +} + +inline static GObject * +eti_a11y_get_gobject (AtkObject *accessible) +{ + return atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (accessible)); +} + +static void +eti_a11y_reset_focus_object (GalA11yETableItem *a11y, + ETableItem *item, + gboolean notify) +{ + ESelectionModel * esm; + gint cursor_row, cursor_col, view_row, view_col; + AtkObject *cell, *old_cell; + + esm = item->selection; + g_return_if_fail (esm); + + cursor_row = e_selection_model_cursor_row (esm); + cursor_col = e_selection_model_cursor_col (esm); + + view_row = model_to_view_row (item, cursor_row); + view_col = model_to_view_col (item, cursor_col); + + if (view_row == -1) + view_row = 0; + if (view_col == -1) + view_col = 0; + + old_cell = (AtkObject *) g_object_get_data (G_OBJECT (a11y), "gail-focus-object"); + if (old_cell && GAL_A11Y_IS_E_CELL (old_cell)) + gal_a11y_e_cell_remove_state ( + GAL_A11Y_E_CELL (old_cell), ATK_STATE_FOCUSED, FALSE); + if (old_cell) + g_object_unref (old_cell); + + cell = eti_ref_at (ATK_TABLE (a11y), view_row, view_col); + + if (cell != NULL) { + g_object_set_data (G_OBJECT (a11y), "gail-focus-object", cell); + gal_a11y_e_cell_add_state ( + GAL_A11Y_E_CELL (cell), ATK_STATE_FOCUSED, FALSE); + } else + g_object_set_data (G_OBJECT (a11y), "gail-focus-object", NULL); + + if (notify && cell) + atk_focus_tracker_notify (cell); +} + +static void +eti_dispose (GObject *object) +{ + GalA11yETableItem *a11y = GAL_A11Y_E_TABLE_ITEM (object); + GalA11yETableItemPrivate *priv = GET_PRIVATE (a11y); + + if (priv->columns) { + free_columns (priv->columns); + priv->columns = NULL; + } + + if (priv->item) { + g_object_weak_unref (G_OBJECT (priv->item), item_finalized, a11y); + priv->item = NULL; + } + + if (parent_class->dispose) + parent_class->dispose (object); +} + +/* Static functions */ +static gint +eti_get_n_children (AtkObject *accessible) +{ + g_return_val_if_fail (GAL_A11Y_IS_E_TABLE_ITEM (accessible), 0); + if (!eti_a11y_get_gobject (accessible)) + return 0; + + return atk_table_get_n_columns (ATK_TABLE (accessible)) * + (atk_table_get_n_rows (ATK_TABLE (accessible)) + 1); +} + +static AtkObject * +eti_ref_child (AtkObject *accessible, + gint index) +{ + ETableItem *item; + gint col, row; + + g_return_val_if_fail (GAL_A11Y_IS_E_TABLE_ITEM (accessible), NULL); + item = E_TABLE_ITEM (eti_a11y_get_gobject (accessible)); + if (!item) + return NULL; + + if (index < item->cols) { + ETableCol *ecol; + AtkObject *child; + + ecol = e_table_header_get_column (item->header, index); + child = gal_a11y_e_table_column_header_new (ecol, item); + return child; + } + index -= item->cols; + + col = index % item->cols; + row = index / item->cols; + + return eti_ref_at (ATK_TABLE (accessible), row, col); +} + +static void +eti_get_extents (AtkComponent *component, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coord_type) +{ + ETableItem *item; + AtkObject *parent; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (component))); + if (!item) + return; + + parent = ATK_OBJECT (component)->accessible_parent; + if (parent && ATK_IS_COMPONENT (parent)) + atk_component_get_extents ( + ATK_COMPONENT (parent), + x, y, + width, height, + coord_type); + + if (parent && GAL_A11Y_IS_E_TABLE_CLICK_TO_ADD (parent)) { + ETableClickToAdd *etcta = E_TABLE_CLICK_TO_ADD ( + atk_gobject_accessible_get_object ( + ATK_GOBJECT_ACCESSIBLE (parent))); + if (etcta) { + *width = etcta->width; + *height = etcta->height; + } + } +} + +static AtkObject * +eti_ref_accessible_at_point (AtkComponent *component, + gint x, + gint y, + AtkCoordType coord_type) +{ + gint row = -1; + gint col = -1; + gint x_origin, y_origin; + ETableItem *item; + GtkWidget *tableOrTree; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (component))); + if (!item) + return NULL; + + atk_component_get_position ( + component, + &x_origin, + &y_origin, + coord_type); + x -= x_origin; + y -= y_origin; + + tableOrTree = gtk_widget_get_parent (GTK_WIDGET (item->parent.canvas)); + + if (E_IS_TREE (tableOrTree)) + e_tree_get_cell_at (E_TREE (tableOrTree), x, y, &row, &col); + else + e_table_get_cell_at (E_TABLE (tableOrTree), x, y, &row, &col); + + if (row != -1 && col != -1) { + return eti_ref_at (ATK_TABLE (component), row, col); + } else { + return NULL; + } +} + +static void +cell_destroyed (gpointer data) +{ + GalA11yECell * cell; + + g_return_if_fail (GAL_A11Y_IS_E_CELL (data)); + cell = GAL_A11Y_E_CELL (data); + + g_return_if_fail (cell->item && G_IS_OBJECT (cell->item)); + + if (cell->item) { + g_object_unref (cell->item); + cell->item = NULL; + } + +} + +/* atk table */ +static AtkObject * +eti_ref_at (AtkTable *table, + gint row, + gint column) +{ + ETableItem *item; + AtkObject * ret; + GalA11yETableItemPrivate *priv = GET_PRIVATE (table); + + if (atk_state_set_contains_state (priv->state_set, ATK_STATE_DEFUNCT)) + return NULL; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return NULL; + + if (column >= 0 && + column < item->cols && + row >= 0 && + row < item->rows && + item->cell_views_realized) { + ECellView *cell_view = item->cell_views[column]; + ETableCol *ecol = e_table_header_get_column (item->header, column); + ret = gal_a11y_e_cell_registry_get_object ( + NULL, + item, + cell_view, + ATK_OBJECT (table), + ecol->col_idx, + column, + row); + if (ATK_IS_OBJECT (ret)) { + g_object_weak_ref ( + G_OBJECT (ret), + (GWeakNotify) cell_destroyed, + ret); + /* if current cell is focused, add FOCUSED state */ + if (e_selection_model_cursor_row (item->selection) == + GAL_A11Y_E_CELL (ret)->row && + e_selection_model_cursor_col (item->selection) == + GAL_A11Y_E_CELL (ret)->model_col) + gal_a11y_e_cell_add_state ( + GAL_A11Y_E_CELL (ret), + ATK_STATE_FOCUSED, FALSE); + } else + ret = NULL; + + return ret; + } + + return NULL; +} + +static gint +eti_get_index_at (AtkTable *table, + gint row, + gint column) +{ + ETableItem *item; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return -1; + + return column + (row + 1) * item->cols; +} + +static gint +eti_get_column_at_index (AtkTable *table, + gint index) +{ + ETableItem *item; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return -1; + + return index % item->cols; +} + +static gint +eti_get_row_at_index (AtkTable *table, + gint index) +{ + ETableItem *item; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return -1; + + return index / item->cols - 1; +} + +static gint +eti_get_n_columns (AtkTable *table) +{ + ETableItem *item; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return -1; + + return item->cols; +} + +static gint +eti_get_n_rows (AtkTable *table) +{ + ETableItem *item; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return -1; + + return item->rows; +} + +static gint +eti_get_column_extent_at (AtkTable *table, + gint row, + gint column) +{ + ETableItem *item; + gint width; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return -1; + + e_table_item_get_cell_geometry ( + item, + &row, + &column, + NULL, + NULL, + &width, + NULL); + + return width; +} + +static gint +eti_get_row_extent_at (AtkTable *table, + gint row, + gint column) +{ + ETableItem *item; + gint height; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return -1; + + e_table_item_get_cell_geometry ( + item, + &row, + &column, + NULL, + NULL, + NULL, + &height); + + return height; +} + +static AtkObject * +eti_get_caption (AtkTable *table) +{ + /* Unimplemented */ + return NULL; +} + +static const gchar * +eti_get_column_description (AtkTable *table, + gint column) +{ + ETableItem *item; + ETableCol *ecol; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return NULL; + + ecol = e_table_header_get_column (item->header, column); + + return ecol->text; +} + +static AtkObject * +eti_get_column_header (AtkTable *table, + gint column) +{ + ETableItem *item; + ETableCol *ecol; + AtkObject *atk_obj = NULL; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return NULL; + + ecol = e_table_header_get_column (item->header, column); + if (ecol) { + atk_obj = gal_a11y_e_table_column_header_new (ecol, item); + } + + return atk_obj; +} + +static const gchar * +eti_get_row_description (AtkTable *table, + gint row) +{ + /* Unimplemented */ + return NULL; +} + +static AtkObject * +eti_get_row_header (AtkTable *table, + gint row) +{ + /* Unimplemented */ + return NULL; +} + +static AtkObject * +eti_get_summary (AtkTable *table) +{ + /* Unimplemented */ + return NULL; +} + +static gboolean +table_is_row_selected (AtkTable *table, + gint row) +{ + ETableItem *item; + GalA11yETableItemPrivate *priv = GET_PRIVATE (table); + + if (row < 0) + return FALSE; + + if (atk_state_set_contains_state (priv->state_set, ATK_STATE_DEFUNCT)) + return FALSE; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return FALSE; + + return e_selection_model_is_row_selected ( + item->selection, view_to_model_row (item, row)); +} + +static gboolean +table_is_selected (AtkTable *table, + gint row, + gint column) +{ + return table_is_row_selected (table, row); +} + +static gint +table_get_selected_rows (AtkTable *table, + gint **rows_selected) +{ + ETableItem *item; + gint n_selected, row, index_selected; + GalA11yETableItemPrivate *priv = GET_PRIVATE (table); + + if (atk_state_set_contains_state (priv->state_set, ATK_STATE_DEFUNCT)) + return 0; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return 0; + + n_selected = e_selection_model_selected_count (item->selection); + if (rows_selected) { + *rows_selected = (gint *) g_malloc (n_selected * sizeof (gint)); + + index_selected = 0; + for (row = 0; row < item->rows && index_selected < n_selected; ++row) { + if (atk_table_is_row_selected (table, row)) { + (*rows_selected)[index_selected] = row; + ++index_selected; + } + } + } + return n_selected; +} + +static gboolean +table_add_row_selection (AtkTable *table, + gint row) +{ + ETableItem *item; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return FALSE; + + if (table_is_row_selected (table, row)) + return TRUE; + e_selection_model_toggle_single_row ( + item->selection, + view_to_model_row (item, row)); + + return TRUE; +} + +static gboolean +table_remove_row_selection (AtkTable *table, + gint row) +{ + ETableItem *item; + GalA11yETableItemPrivate *priv = GET_PRIVATE (table); + + if (atk_state_set_contains_state (priv->state_set, ATK_STATE_DEFUNCT)) + return FALSE; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (table))); + if (!item) + return FALSE; + + if (!atk_table_is_row_selected (table, row)) + return TRUE; + + e_selection_model_toggle_single_row ( + item->selection, view_to_model_row (item, row)); + + return TRUE; +} + +static void +eti_atk_table_iface_init (AtkTableIface *iface) +{ + iface->ref_at = eti_ref_at; + iface->get_index_at = eti_get_index_at; + iface->get_column_at_index = eti_get_column_at_index; + iface->get_row_at_index = eti_get_row_at_index; + iface->get_n_columns = eti_get_n_columns; + iface->get_n_rows = eti_get_n_rows; + iface->get_column_extent_at = eti_get_column_extent_at; + iface->get_row_extent_at = eti_get_row_extent_at; + iface->get_caption = eti_get_caption; + iface->get_column_description = eti_get_column_description; + iface->get_column_header = eti_get_column_header; + iface->get_row_description = eti_get_row_description; + iface->get_row_header = eti_get_row_header; + iface->get_summary = eti_get_summary; + + iface->is_row_selected = table_is_row_selected; + iface->is_selected = table_is_selected; + iface->get_selected_rows = table_get_selected_rows; + iface->add_row_selection = table_add_row_selection; + iface->remove_row_selection = table_remove_row_selection; +} + +static void +eti_atk_component_iface_init (AtkComponentIface *iface) +{ + component_parent_iface = g_type_interface_peek_parent (iface); + + iface->ref_accessible_at_point = eti_ref_accessible_at_point; + iface->get_extents = eti_get_extents; +} + +static void +eti_rows_inserted (ETableModel *model, + gint row, + gint count, + AtkObject *table_item) +{ + gint n_cols,n_rows,i,j; + GalA11yETableItem * item_a11y; + gint old_nrows; + + g_return_if_fail (table_item); + item_a11y = GAL_A11Y_E_TABLE_ITEM (table_item); + + n_cols = atk_table_get_n_columns (ATK_TABLE (table_item)); + n_rows = atk_table_get_n_rows (ATK_TABLE (table_item)); + + old_nrows = GET_PRIVATE (item_a11y)->rows; + + g_return_if_fail (n_cols > 0 && n_rows > 0); + g_return_if_fail (old_nrows == n_rows - count); + + GET_PRIVATE (table_item)->rows = n_rows; + + g_signal_emit_by_name ( + table_item, "row-inserted", row, + count, NULL); + + for (i = row; i < (row + count); i++) { + for (j = 0; j < n_cols; j++) { + g_signal_emit_by_name ( + table_item, + "children_changed::add", + (((i + 1) * n_cols) + j), NULL, NULL); + } + } + + g_signal_emit_by_name (table_item, "visible-data-changed"); +} + +static void +eti_rows_deleted (ETableModel *model, + gint row, + gint count, + AtkObject *table_item) +{ + gint i,j, n_rows, n_cols, old_nrows; + ETableItem *item = E_TABLE_ITEM ( + atk_gobject_accessible_get_object ( + ATK_GOBJECT_ACCESSIBLE (table_item))); + + n_rows = atk_table_get_n_rows (ATK_TABLE (table_item)); + n_cols = atk_table_get_n_columns (ATK_TABLE (table_item)); + + old_nrows = GET_PRIVATE (table_item)->rows; + + g_return_if_fail (row + count <= old_nrows); + g_return_if_fail (old_nrows == n_rows + count); + GET_PRIVATE (table_item)->rows = n_rows; + + g_signal_emit_by_name ( + table_item, "row-deleted", row, + count, NULL); + + for (i = row; i < (row + count); i++) { + for (j = 0; j < n_cols; j++) { + g_signal_emit_by_name ( + table_item, + "children_changed::remove", + (((i + 1) * n_cols) + j), NULL, NULL); + } + } + g_signal_emit_by_name (table_item, "visible-data-changed"); + eti_a11y_reset_focus_object ((GalA11yETableItem *) table_item, item, TRUE); +} + +static void +eti_tree_model_node_changed_cb (ETreeModel *model, + ETreePath node, + ETableItem *eti) +{ + AtkObject *atk_obj; + GalA11yETableItem *a11y; + + g_return_if_fail (E_IS_TABLE_ITEM (eti)); + + atk_obj = atk_gobject_accessible_for_object (G_OBJECT (eti)); + a11y = GAL_A11Y_E_TABLE_ITEM (atk_obj); + + /* we can't figure out which rows are changed, so just send out a signal ... */ + if (GET_PRIVATE (a11y)->rows > 0) + g_signal_emit_by_name (a11y, "visible-data-changed"); +} + +enum { + ETI_HEADER_UNCHANGED = 0, + ETI_HEADER_REORDERED, + ETI_HEADER_NEW_ADDED, + ETI_HEADER_REMOVED +}; + +/* + * 1. Check what actually happened: column reorder, remove or add + * 2. Update cache + * 3. Emit signals + */ +static void +eti_header_structure_changed (ETableHeader *eth, + AtkObject *a11y) +{ + + gboolean reorder_found = FALSE, added_found = FALSE, removed_found = FALSE; + GalA11yETableItem * a11y_item; + ETableCol ** cols, **prev_cols; + GalA11yETableItemPrivate *priv; + gint *state = NULL, *prev_state = NULL, *reorder = NULL; + gint i,j,n_rows,n_cols, prev_n_cols; + + a11y_item = GAL_A11Y_E_TABLE_ITEM (a11y); + priv = GET_PRIVATE (a11y_item); + + /* Assume rows do not changed. */ + n_rows = priv->rows; + + prev_n_cols = priv->cols; + prev_cols = priv->columns; + + cols = e_table_header_get_columns (eth); + n_cols = eth->col_count; + + g_return_if_fail (cols && prev_cols && n_cols > 0); + + /* Init to ETI_HEADER_UNCHANGED. */ + state = g_malloc0 (sizeof (gint) * n_cols); + prev_state = g_malloc0 (sizeof (gint) * prev_n_cols); + reorder = g_malloc0 (sizeof (gint) * n_cols); + + /* Compare with previously saved column headers. */ + for (i = 0; i < n_cols && cols[i]; i++) { + for (j = 0; j < prev_n_cols && prev_cols[j]; j++) { + if (prev_cols[j] == cols[i] && i != j) { + + reorder_found = TRUE; + state[i] = ETI_HEADER_REORDERED; + reorder[i] = j; + + break; + } else if (prev_cols[j] == cols[i]) { + /* OK, this column is not changed. */ + break; + } + } + + /* cols[i] is new added column. */ + if (j == prev_n_cols) { + added_found = TRUE; + state[i] = ETI_HEADER_NEW_ADDED; + } + } + + /* Now try to find if there are removed columns. */ + for (i = 0; i < prev_n_cols && prev_cols[i]; i++) { + for (j = 0; j < n_cols && cols[j]; j++) + if (prev_cols[j] == cols[i]) + break; + + /* Removed columns found. */ + if (j == n_cols) { + removed_found = TRUE; + prev_state[j] = ETI_HEADER_REMOVED; + } + } + + /* If nothing interesting just return. */ + if (!reorder_found && !added_found && !removed_found) + return; + + /* Emit signals */ + if (reorder_found) + g_signal_emit_by_name (a11y_item, "column_reordered"); + + if (removed_found) { + for (i = 0; i < prev_n_cols; i++) { + if (prev_state[i] == ETI_HEADER_REMOVED) { + g_signal_emit_by_name ( + a11y_item, "column-deleted", i, 1); + for (j = 0; j < n_rows; j++) + g_signal_emit_by_name ( + a11y_item, + "children_changed::remove", + ((j + 1) * prev_n_cols + i), + NULL, NULL); + } + } + } + + if (added_found) { + for (i = 0; i < n_cols; i++) { + if (state[i] == ETI_HEADER_NEW_ADDED) { + g_signal_emit_by_name ( + a11y_item, "column-inserted", i, 1); + for (j = 0; j < n_rows; j++) + g_signal_emit_by_name ( + a11y_item, + "children_changed::add", + ((j + 1) * n_cols + i), + NULL, NULL); + } + } + } + + priv->cols = n_cols; + + g_free (state); + g_free (reorder); + g_free (prev_state); + + free_columns (priv->columns); + priv->columns = cols; +} + +static void +eti_real_initialize (AtkObject *obj, + gpointer data) +{ + ETableItem * eti; + ETableModel * model; + + ATK_OBJECT_CLASS (parent_class)->initialize (obj, data); + eti = E_TABLE_ITEM (data); + + model = eti->table_model; + + g_signal_connect ( + model, "model-rows-inserted", + G_CALLBACK (eti_rows_inserted), obj); + g_signal_connect ( + model, "model-rows-deleted", + G_CALLBACK (eti_rows_deleted), obj); + g_signal_connect ( + eti->header, "structure_change", + G_CALLBACK (eti_header_structure_changed), obj); + +} + +static void +eti_class_init (GalA11yETableItemClass *class) +{ + AtkObjectClass *atk_object_class = ATK_OBJECT_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + quark_accessible_object = + g_quark_from_static_string ("gtk-accessible-object"); + + parent_class = g_type_class_ref (PARENT_TYPE); + + object_class->dispose = eti_dispose; + + atk_object_class->get_n_children = eti_get_n_children; + atk_object_class->ref_child = eti_ref_child; + atk_object_class->initialize = eti_real_initialize; + atk_object_class->ref_state_set = eti_ref_state_set; +} + +static void +eti_init (GalA11yETableItem *a11y) +{ + GalA11yETableItemPrivate *priv; + + priv = GET_PRIVATE (a11y); + + priv->selection_change_id = 0; + priv->cursor_change_id = 0; + priv->selection = NULL; +} + +/* atk selection */ + +static void atk_selection_interface_init (AtkSelectionIface *iface); +static gboolean selection_add_selection (AtkSelection *selection, + gint i); +static gboolean selection_clear_selection (AtkSelection *selection); +static AtkObject * + selection_ref_selection (AtkSelection *selection, + gint i); +static gint selection_get_selection_count (AtkSelection *selection); +static gboolean selection_is_child_selected (AtkSelection *selection, + gint i); + +/* callbacks */ +static void eti_a11y_selection_model_removed_cb (ETableItem *eti, + ESelectionModel *selection, + gpointer data); +static void eti_a11y_selection_model_added_cb (ETableItem *eti, + ESelectionModel *selection, + gpointer data); +static void eti_a11y_selection_changed_cb (ESelectionModel *selection, + GalA11yETableItem *a11y); +static void eti_a11y_cursor_changed_cb (ESelectionModel *selection, + gint row, gint col, + GalA11yETableItem *a11y); + +/** + * gal_a11y_e_table_item_get_type: + * @void: + * + * Registers the &GalA11yETableItem class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yETableItem class. + **/ +GType +gal_a11y_e_table_item_get_type (void) +{ + static GType type = 0; + + if (!type) { + AtkObjectFactory *factory; + + GTypeInfo info = { + sizeof (GalA11yETableItemClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) eti_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yETableItem), + 0, + (GInstanceInitFunc) eti_init, + NULL /* value_table_item */ + }; + + static const GInterfaceInfo atk_component_info = { + (GInterfaceInitFunc) eti_atk_component_iface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + static const GInterfaceInfo atk_table_info = { + (GInterfaceInitFunc) eti_atk_table_iface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + static const GInterfaceInfo atk_selection_info = { + (GInterfaceInitFunc) atk_selection_interface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + factory = atk_registry_get_factory ( + atk_get_default_registry (), GNOME_TYPE_CANVAS_ITEM); + parent_type = atk_object_factory_get_accessible_type (factory); + + type = gal_a11y_type_register_static_with_private ( + PARENT_TYPE, "GalA11yETableItem", &info, 0, + sizeof (GalA11yETableItemPrivate), &priv_offset); + + g_type_add_interface_static (type, ATK_TYPE_COMPONENT, &atk_component_info); + g_type_add_interface_static (type, ATK_TYPE_TABLE, &atk_table_info); + g_type_add_interface_static (type, ATK_TYPE_SELECTION, &atk_selection_info); + } + + return type; +} + +AtkObject * +gal_a11y_e_table_item_new (ETableItem *item) +{ + GalA11yETableItem *a11y; + AtkObject *accessible; + ESelectionModel * esm; + AtkObject *parent; + const gchar *name; + + g_return_val_if_fail (item && item->cols >= 0 && item->rows >= 0, NULL); + a11y = g_object_new (gal_a11y_e_table_item_get_type (), NULL); + + atk_object_initialize (ATK_OBJECT (a11y), item); + + GET_PRIVATE (a11y)->state_set = atk_state_set_new (); + + atk_state_set_add_state (GET_PRIVATE (a11y)->state_set, ATK_STATE_TRANSIENT); + atk_state_set_add_state (GET_PRIVATE (a11y)->state_set, ATK_STATE_ENABLED); + atk_state_set_add_state (GET_PRIVATE (a11y)->state_set, ATK_STATE_SENSITIVE); + atk_state_set_add_state (GET_PRIVATE (a11y)->state_set, ATK_STATE_SHOWING); + atk_state_set_add_state (GET_PRIVATE (a11y)->state_set, ATK_STATE_VISIBLE); + + accessible = ATK_OBJECT (a11y); + + GET_PRIVATE (a11y)->item = item; + /* Initialize cell data. */ + GET_PRIVATE (a11y)->cols = item->cols; + GET_PRIVATE (a11y)->rows = item->rows; + + GET_PRIVATE (a11y)->columns = e_table_header_get_columns (item->header); + if (GET_PRIVATE (a11y)->columns == NULL) + return NULL; + + if (item) { + g_signal_connect ( + item, "selection_model_removed", + G_CALLBACK (eti_a11y_selection_model_removed_cb), NULL); + g_signal_connect ( + item, "selection_model_added", + G_CALLBACK (eti_a11y_selection_model_added_cb), NULL); + if (item->selection) + gal_a11y_e_table_item_ref_selection ( + a11y, + item->selection); + + /* find the TableItem's parent: table or tree */ + GET_PRIVATE (a11y)->widget = gtk_widget_get_parent ( + GTK_WIDGET (item->parent.canvas)); + parent = gtk_widget_get_accessible (GET_PRIVATE (a11y)->widget); + name = atk_object_get_name (parent); + if (name) + atk_object_set_name (accessible, name); + atk_object_set_parent (accessible, parent); + + if (E_IS_TREE (GET_PRIVATE (a11y)->widget)) { + ETreeModel *model; + model = e_tree_get_model (E_TREE (GET_PRIVATE (a11y)->widget)); + g_signal_connect ( + model, "node_changed", + G_CALLBACK (eti_tree_model_node_changed_cb), item); + accessible->role = ATK_ROLE_TREE_TABLE; + } else if (E_IS_TABLE (GET_PRIVATE (a11y)->widget)) { + accessible->role = ATK_ROLE_TABLE; + } + } + + if (item) + g_object_weak_ref (G_OBJECT (item), item_finalized, g_object_ref (a11y)); + + esm = item->selection; + + if (esm != NULL) { + eti_a11y_reset_focus_object (a11y, item, FALSE); + } + + return ATK_OBJECT (a11y); +} + +static gboolean +gal_a11y_e_table_item_ref_selection (GalA11yETableItem *a11y, + ESelectionModel *selection) +{ + GalA11yETableItemPrivate *priv; + + g_return_val_if_fail (a11y && selection, FALSE); + + priv = GET_PRIVATE (a11y); + priv->selection_change_id = g_signal_connect ( + selection, "selection_changed", + G_CALLBACK (eti_a11y_selection_changed_cb), a11y); + priv->cursor_change_id = g_signal_connect ( + selection, "cursor_changed", + G_CALLBACK (eti_a11y_cursor_changed_cb), a11y); + + priv->selection = selection; + g_object_ref (selection); + + return TRUE; +} + +static gboolean +gal_a11y_e_table_item_unref_selection (GalA11yETableItem *a11y) +{ + GalA11yETableItemPrivate *priv; + + g_return_val_if_fail (a11y, FALSE); + + priv = GET_PRIVATE (a11y); + + g_return_val_if_fail (priv->selection_change_id != 0, FALSE); + g_return_val_if_fail (priv->cursor_change_id != 0, FALSE); + + g_signal_handler_disconnect ( + priv->selection, + priv->selection_change_id); + g_signal_handler_disconnect ( + priv->selection, + priv->cursor_change_id); + priv->cursor_change_id = 0; + priv->selection_change_id = 0; + + g_object_unref (priv->selection); + priv->selection = NULL; + + return TRUE; +} + +/* callbacks */ + +static void +eti_a11y_selection_model_removed_cb (ETableItem *eti, + ESelectionModel *selection, + gpointer data) +{ + AtkObject *atk_obj; + GalA11yETableItem *a11y; + + g_return_if_fail (E_IS_TABLE_ITEM (eti)); + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + atk_obj = atk_gobject_accessible_for_object (G_OBJECT (eti)); + a11y = GAL_A11Y_E_TABLE_ITEM (atk_obj); + + if (selection == GET_PRIVATE (a11y)->selection) + gal_a11y_e_table_item_unref_selection (a11y); +} + +static void +eti_a11y_selection_model_added_cb (ETableItem *eti, + ESelectionModel *selection, + gpointer data) +{ + AtkObject *atk_obj; + GalA11yETableItem *a11y; + + g_return_if_fail (E_IS_TABLE_ITEM (eti)); + g_return_if_fail (E_IS_SELECTION_MODEL (selection)); + + atk_obj = atk_gobject_accessible_for_object (G_OBJECT (eti)); + a11y = GAL_A11Y_E_TABLE_ITEM (atk_obj); + + if (GET_PRIVATE (a11y)->selection) + gal_a11y_e_table_item_unref_selection (a11y); + gal_a11y_e_table_item_ref_selection (a11y, selection); +} + +static void +eti_a11y_selection_changed_cb (ESelectionModel *selection, + GalA11yETableItem *a11y) +{ + GalA11yETableItemPrivate *priv = GET_PRIVATE (a11y); + + if (atk_state_set_contains_state (priv->state_set, ATK_STATE_DEFUNCT)) + return; + + g_return_if_fail (GAL_A11Y_IS_E_TABLE_ITEM (a11y)); + + g_signal_emit_by_name (a11y, "selection_changed"); +} + +static void +eti_a11y_cursor_changed_cb (ESelectionModel *selection, + gint row, + gint col, + GalA11yETableItem *a11y) +{ + ETableItem *item; + GalA11yETableItemPrivate *priv = GET_PRIVATE (a11y); + + g_return_if_fail (GAL_A11Y_IS_E_TABLE_ITEM (a11y)); + + if (atk_state_set_contains_state (priv->state_set, ATK_STATE_DEFUNCT)) + return; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (a11y))); + + g_return_if_fail (item); + + if (row == -1 && col == -1) + return; + eti_a11y_reset_focus_object (a11y, item, TRUE); +} + +/* atk selection */ + +static void atk_selection_interface_init (AtkSelectionIface *iface) +{ + g_return_if_fail (iface != NULL); + iface->add_selection = selection_add_selection; + iface->clear_selection = selection_clear_selection; + iface->ref_selection = selection_ref_selection; + iface->get_selection_count = selection_get_selection_count; + iface->is_child_selected = selection_is_child_selected; +} + +static gboolean +selection_add_selection (AtkSelection *selection, + gint index) +{ + AtkTable *table; + gint row, col, cursor_row, cursor_col, model_row, model_col; + ETableItem *item; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (selection))); + if (!item) + return FALSE; + + table = ATK_TABLE (selection); + + row = atk_table_get_row_at_index (table, index); + col = atk_table_get_column_at_index (table, index); + + model_row = view_to_model_row (item, row); + model_col = view_to_model_col (item, col); + + cursor_row = e_selection_model_cursor_row (item->selection); + cursor_col = e_selection_model_cursor_col (item->selection); + + /* check whether is selected already */ + if (model_row == cursor_row && model_col == cursor_col) + return TRUE; + + if (model_row != cursor_row) { + /* we need to make the item get focus */ + e_canvas_item_grab_focus (GNOME_CANVAS_ITEM (item), TRUE); + + /* FIXME, currently we only support single row selection */ + atk_selection_clear_selection (selection); + atk_table_add_row_selection (table, row); + } + + e_selection_model_change_cursor ( + item->selection, + model_row, + model_col); + e_selection_model_cursor_changed ( + item->selection, + model_row, + model_col); + e_selection_model_cursor_activated ( + item->selection, + model_row, + model_col); + return TRUE; +} + +static gboolean +selection_clear_selection (AtkSelection *selection) +{ + ETableItem *item; + + item = E_TABLE_ITEM (eti_a11y_get_gobject (ATK_OBJECT (selection))); + if (!item) + return FALSE; + + e_selection_model_clear (item->selection); + return TRUE; +} + +static AtkObject * +selection_ref_selection (AtkSelection *selection, + gint index) +{ + AtkTable *table; + gint row, col; + + table = ATK_TABLE (selection); + row = atk_table_get_row_at_index (table, index); + col = atk_table_get_column_at_index (table, index); + if (!atk_table_is_row_selected (table, row)) + return NULL; + + return eti_ref_at (table, row, col); +} + +static gint +selection_get_selection_count (AtkSelection *selection) +{ + AtkTable *table; + gint n_selected; + + table = ATK_TABLE (selection); + n_selected = atk_table_get_selected_rows (table, NULL); + if (n_selected > 0) + n_selected *= atk_table_get_n_columns (table); + return n_selected; +} + +static gboolean +selection_is_child_selected (AtkSelection *selection, + gint i) +{ + gint row; + + row = atk_table_get_row_at_index (ATK_TABLE (selection), i); + return atk_table_is_row_selected (ATK_TABLE (selection), row); +} + +void +gal_a11y_e_table_item_init (void) +{ + if (atk_get_root ()) + atk_registry_set_factory_type ( + atk_get_default_registry (), + E_TYPE_TABLE_ITEM, + gal_a11y_e_table_item_factory_get_type ()); +} + diff --git a/e-util/gal-a11y-e-table-item.h b/e-util/gal-a11y-e-table-item.h new file mode 100644 index 0000000000..4791a70354 --- /dev/null +++ b/e-util/gal-a11y-e-table-item.h @@ -0,0 +1,62 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TABLE_ITEM_H__ +#define __GAL_A11Y_E_TABLE_ITEM_H__ + +#include <atk/atkgobjectaccessible.h> + +#include <e-util/e-table-item.h> + +#define GAL_A11Y_TYPE_E_TABLE_ITEM (gal_a11y_e_table_item_get_type ()) +#define GAL_A11Y_E_TABLE_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TABLE_ITEM, GalA11yETableItem)) +#define GAL_A11Y_E_TABLE_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TABLE_ITEM, GalA11yETableItemClass)) +#define GAL_A11Y_IS_E_TABLE_ITEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TABLE_ITEM)) +#define GAL_A11Y_IS_E_TABLE_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TABLE_ITEM)) + +typedef struct _GalA11yETableItem GalA11yETableItem; +typedef struct _GalA11yETableItemClass GalA11yETableItemClass; +typedef struct _GalA11yETableItemPrivate GalA11yETableItemPrivate; + +/* This struct should actually be larger as this isn't what we derive from. + * The GalA11yETableItemPrivate comes right after the parent class structure. + **/ +struct _GalA11yETableItem { + AtkGObjectAccessible parent; +}; + +struct _GalA11yETableItemClass { + AtkGObjectAccessibleClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_table_item_get_type (void); +AtkObject *gal_a11y_e_table_item_new (ETableItem *item); + +void gal_a11y_e_table_item_init (void); + +#endif /* __GAL_A11Y_E_TABLE_ITEM_H__ */ diff --git a/e-util/gal-a11y-e-table.c b/e-util/gal-a11y-e-table.c new file mode 100644 index 0000000000..f9178bd144 --- /dev/null +++ b/e-util/gal-a11y-e-table.c @@ -0,0 +1,315 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-table.h" + +#include "e-table-click-to-add.h" +#include "e-table-group-container.h" +#include "e-table-group-leaf.h" +#include "e-table-group.h" +#include "e-table.h" +#include "gal-a11y-e-table-factory.h" +#include "gal-a11y-e-table-item.h" +#include "gal-a11y-util.h" + +#define CS_CLASS(a11y) (G_TYPE_INSTANCE_GET_CLASS ((a11y), C_TYPE_STREAM, GalA11yETableClass)) +static AtkObjectClass *parent_class; +static GType parent_type; +static gint priv_offset; +#define GET_PRIVATE(object) ((GalA11yETablePrivate *) (((gchar *) object) + priv_offset)) +#define PARENT_TYPE (parent_type) + +struct _GalA11yETablePrivate { + AtkObject *child_item; +}; + +/* Static functions */ +static ETableItem * +find_first_table_item (ETableGroup *group) +{ + GnomeCanvasGroup *cgroup; + GList *l; + + cgroup = GNOME_CANVAS_GROUP (group); + + for (l = cgroup->item_list; l; l = l->next) { + GnomeCanvasItem *i; + + i = GNOME_CANVAS_ITEM (l->data); + + if (E_IS_TABLE_GROUP (i)) + return find_first_table_item (E_TABLE_GROUP (i)); + else if (E_IS_TABLE_ITEM (i)) { + return E_TABLE_ITEM (i); + } + } + + return NULL; +} + +static AtkObject * +eti_get_accessible (ETableItem *eti, + AtkObject *parent) +{ + AtkObject *a11y = NULL; + + g_return_val_if_fail (eti, NULL); + + a11y = atk_gobject_accessible_for_object (G_OBJECT (eti)); + g_return_val_if_fail (a11y, NULL); + + return a11y; +} + +static gboolean +init_child_item (GalA11yETable *a11y) +{ + ETable *table; + + if (!a11y || !GTK_IS_ACCESSIBLE (a11y)) + return FALSE; + + table = E_TABLE (gtk_accessible_get_widget (GTK_ACCESSIBLE (a11y))); + if (table && gtk_widget_get_mapped (GTK_WIDGET (table)) && table->group && E_IS_TABLE_GROUP_CONTAINER (table->group)) { + ETableGroupContainer *etgc = (ETableGroupContainer *) table->group; + GList *list; + + for (list = etgc->children; list; list = g_list_next (list)) { + ETableGroupContainerChildNode *child_node = list->data; + ETableGroup *child = child_node->child; + ETableItem *eti = find_first_table_item (child); + + eti_get_accessible (eti, ATK_OBJECT (a11y)); + } + } + g_object_unref (a11y); + g_object_unref (table); + + return FALSE; +} + +static AtkObject * +et_ref_accessible_at_point (AtkComponent *component, + gint x, + gint y, + AtkCoordType coord_type) +{ + GalA11yETable *a11y = GAL_A11Y_E_TABLE (component); + if (GET_PRIVATE (a11y)->child_item) + g_object_ref (GET_PRIVATE (a11y)->child_item); + return GET_PRIVATE (a11y)->child_item; +} + +static gint +et_get_n_children (AtkObject *accessible) +{ + GalA11yETable *a11y = GAL_A11Y_E_TABLE (accessible); + ETable * et; + gint n = 0; + + et = E_TABLE (gtk_accessible_get_widget (GTK_ACCESSIBLE (a11y))); + + if (et && et->group) { + if (E_IS_TABLE_GROUP_LEAF (et->group)) + n = 1; + else if (E_IS_TABLE_GROUP_CONTAINER (et->group)) { + ETableGroupContainer *etgc = (ETableGroupContainer *) et->group; + n = g_list_length (etgc->children); + } + } + + if (et && et->use_click_to_add && et->click_to_add) { + n++; + } + return n; +} + +static AtkObject * +et_ref_child (AtkObject *accessible, + gint i) +{ + GalA11yETable *a11y = GAL_A11Y_E_TABLE (accessible); + ETable * et; + gint child_no; + + et = E_TABLE (gtk_accessible_get_widget (GTK_ACCESSIBLE (a11y))); + if (!et) + return NULL; + + child_no = et_get_n_children (accessible); + if (i == 0 || i < child_no - 1) { + if (E_IS_TABLE_GROUP_LEAF (et->group)) { + ETableItem *eti = find_first_table_item (et->group); + AtkObject *aeti = eti_get_accessible (eti, accessible); + if (aeti) + g_object_ref (aeti); + return aeti; + + } else if (E_IS_TABLE_GROUP_CONTAINER (et->group)) { + ETableGroupContainer *etgc = (ETableGroupContainer *) et->group; + ETableGroupContainerChildNode *child_node = g_list_nth_data (etgc->children, i); + if (child_node) { + ETableGroup *child = child_node->child; + ETableItem * eti = find_first_table_item (child); + AtkObject *aeti = eti_get_accessible (eti, accessible); + if (aeti) + g_object_ref (aeti); + return aeti; + } + } + } else if (i == child_no -1) { + ETableClickToAdd * etcta; + + if (et && et->use_click_to_add && et->click_to_add) { + etcta = E_TABLE_CLICK_TO_ADD (et->click_to_add); + accessible = atk_gobject_accessible_for_object (G_OBJECT (etcta)); + if (accessible) + g_object_ref (accessible); + return accessible; + } + } + + return NULL; +} + +static AtkLayer +et_get_layer (AtkComponent *component) +{ + return ATK_LAYER_WIDGET; +} + +static void +et_class_init (GalA11yETableClass *class) +{ + AtkObjectClass *atk_object_class = ATK_OBJECT_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + atk_object_class->get_n_children = et_get_n_children; + atk_object_class->ref_child = et_ref_child; +} + +static void +et_atk_component_iface_init (AtkComponentIface *iface) +{ + iface->ref_accessible_at_point = et_ref_accessible_at_point; + iface->get_layer = et_get_layer; +} + +static void +et_init (GalA11yETable *a11y) +{ + GalA11yETablePrivate *priv; + + priv = GET_PRIVATE (a11y); + + priv->child_item = NULL; +} + +/** + * gal_a11y_e_table_get_type: + * @void: + * + * Registers the &GalA11yETable class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yETable class. + **/ +GType +gal_a11y_e_table_get_type (void) +{ + static GType type = 0; + + if (!type) { + AtkObjectFactory *factory; + + GTypeInfo info = { + sizeof (GalA11yETableClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) et_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yETable), + 0, + (GInstanceInitFunc) et_init, + NULL /* value_table */ + }; + + static const GInterfaceInfo atk_component_info = { + (GInterfaceInitFunc) et_atk_component_iface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + factory = atk_registry_get_factory (atk_get_default_registry (), GTK_TYPE_WIDGET); + parent_type = atk_object_factory_get_accessible_type (factory); + + type = gal_a11y_type_register_static_with_private ( + PARENT_TYPE, "GalA11yETable", &info, 0, + sizeof (GalA11yETablePrivate), &priv_offset); + g_type_add_interface_static (type, ATK_TYPE_COMPONENT, &atk_component_info); + } + + return type; +} + +AtkObject * +gal_a11y_e_table_new (GObject *widget) +{ + GalA11yETable *a11y; + ETable *table; + + table = E_TABLE (widget); + + a11y = g_object_new (gal_a11y_e_table_get_type (), NULL); + + gtk_accessible_set_widget (GTK_ACCESSIBLE (a11y), GTK_WIDGET (widget)); + + /* we need to init all the children for multiple table items */ + if (table && gtk_widget_get_mapped (GTK_WIDGET (table)) && table->group && E_IS_TABLE_GROUP_CONTAINER (table->group)) { + /* Ref it here so that it is still valid in the idle function */ + /* It will be unrefed in the idle function */ + g_object_ref (a11y); + g_object_ref (widget); + + g_idle_add ((GSourceFunc) init_child_item, a11y); + } + + return ATK_OBJECT (a11y); +} + +void +gal_a11y_e_table_init (void) +{ + if (atk_get_root ()) + atk_registry_set_factory_type ( + atk_get_default_registry (), + E_TYPE_TABLE, + gal_a11y_e_table_factory_get_type ()); + +} + diff --git a/e-util/gal-a11y-e-table.h b/e-util/gal-a11y-e-table.h new file mode 100644 index 0000000000..1e47965af4 --- /dev/null +++ b/e-util/gal-a11y-e-table.h @@ -0,0 +1,62 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TABLE_H__ +#define __GAL_A11Y_E_TABLE_H__ + +#include <gtk/gtk.h> +#include <atk/atkobject.h> +#include <atk/atkcomponent.h> + +#define GAL_A11Y_TYPE_E_TABLE (gal_a11y_e_table_get_type ()) +#define GAL_A11Y_E_TABLE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TABLE, GalA11yETable)) +#define GAL_A11Y_E_TABLE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TABLE, GalA11yETableClass)) +#define GAL_A11Y_IS_E_TABLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TABLE)) +#define GAL_A11Y_IS_E_TABLE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TABLE)) + +typedef struct _GalA11yETable GalA11yETable; +typedef struct _GalA11yETableClass GalA11yETableClass; +typedef struct _GalA11yETablePrivate GalA11yETablePrivate; + +/* This struct should actually be larger as this isn't what we derive from. + * The GalA11yETablePrivate comes right after the parent class structure. + **/ +struct _GalA11yETable { + GtkAccessible object; +}; + +struct _GalA11yETableClass { + GtkAccessibleClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_table_get_type (void); +AtkObject *gal_a11y_e_table_new (GObject *table); + +void gal_a11y_e_table_init (void); + +#endif /* __GAL_A11Y_E_TABLE_H__ */ diff --git a/e-util/gal-a11y-e-text-factory.c b/e-util/gal-a11y-e-text-factory.c new file mode 100644 index 0000000000..191b30d362 --- /dev/null +++ b/e-util/gal-a11y-e-text-factory.c @@ -0,0 +1,103 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-text.h" +#include "gal-a11y-e-text-factory.h" +#include "gal-a11y-e-text.h" + +static AtkObjectFactoryClass *parent_class; +#define PARENT_TYPE (ATK_TYPE_OBJECT_FACTORY) + +/* Static functions */ + +static GType +gal_a11y_e_text_factory_get_accessible_type (void) +{ + return GAL_A11Y_TYPE_E_TEXT; +} + +static AtkObject * +gal_a11y_e_text_factory_create_accessible (GObject *obj) +{ + AtkObject *atk_object; + + g_return_val_if_fail (E_IS_TEXT (obj), NULL); + + atk_object = g_object_new (GAL_A11Y_TYPE_E_TEXT, NULL); + atk_object_initialize (atk_object, obj); + + return atk_object; +} + +static void +gal_a11y_e_text_factory_class_init (GalA11yETextFactoryClass *class) +{ + AtkObjectFactoryClass *factory_class = ATK_OBJECT_FACTORY_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + factory_class->create_accessible = gal_a11y_e_text_factory_create_accessible; + factory_class->get_accessible_type = gal_a11y_e_text_factory_get_accessible_type; +} + +static void +gal_a11y_e_text_factory_init (GalA11yETextFactory *factory) +{ +} + +/** + * gal_a11y_e_text_factory_get_type: + * @void: + * + * Registers the &GalA11yETextFactory class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yETextFactory class. + **/ +GType +gal_a11y_e_text_factory_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yETextFactoryClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) gal_a11y_e_text_factory_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yETextFactory), + 0, + (GInstanceInitFunc) gal_a11y_e_text_factory_init, + NULL /* value_text */ + }; + + type = g_type_register_static (PARENT_TYPE, "GalA11yETextFactory", &info, 0); + } + + return type; +} diff --git a/e-util/gal-a11y-e-text-factory.h b/e-util/gal-a11y-e-text-factory.h new file mode 100644 index 0000000000..4647fe3fcd --- /dev/null +++ b/e-util/gal-a11y-e-text-factory.h @@ -0,0 +1,52 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TEXT_FACTORY_H__ +#define __GAL_A11Y_E_TEXT_FACTORY_H__ + +#include <atk/atkobjectfactory.h> + +#define GAL_A11Y_TYPE_E_TEXT_FACTORY (gal_a11y_e_text_factory_get_type ()) +#define GAL_A11Y_E_TEXT_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TEXT_FACTORY, GalA11yETextFactory)) +#define GAL_A11Y_E_TEXT_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TEXT_FACTORY, GalA11yETextFactoryClass)) +#define GAL_A11Y_IS_E_TEXT_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TEXT_FACTORY)) +#define GAL_A11Y_IS_E_TEXT_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TEXT_FACTORY)) + +typedef struct _GalA11yETextFactory GalA11yETextFactory; +typedef struct _GalA11yETextFactoryClass GalA11yETextFactoryClass; + +struct _GalA11yETextFactory { + AtkObjectFactory object; +}; + +struct _GalA11yETextFactoryClass { + AtkObjectFactoryClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_text_factory_get_type (void); + +#endif /* __GAL_A11Y_E_TEXT_FACTORY_H__ */ diff --git a/e-util/gal-a11y-e-text.c b/e-util/gal-a11y-e-text.c new file mode 100644 index 0000000000..9b03a78483 --- /dev/null +++ b/e-util/gal-a11y-e-text.c @@ -0,0 +1,1141 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-text.h" + +#include <string.h> + +#include <gtk/gtk.h> + +#include "e-text.h" +#include "e-text-model-repos.h" +#include "gal-a11y-e-text-factory.h" +#include "gal-a11y-util.h" + +static GObjectClass *parent_class; +static AtkComponentIface *component_parent_iface; +static GType parent_type; +static gint priv_offset; +static GQuark quark_accessible_object = 0; +#define PARENT_TYPE (parent_type) + +struct _GalA11yETextPrivate { + gint dummy; +}; + +static void +et_dispose (GObject *object) +{ + if (parent_class->dispose) + parent_class->dispose (object); +} + +/* Static functions */ + +static void +et_get_extents (AtkComponent *component, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coord_type) +{ + EText *item = E_TEXT (atk_gobject_accessible_get_object ( + ATK_GOBJECT_ACCESSIBLE (component))); + gdouble real_width; + gdouble real_height; + gint fake_width; + gint fake_height; + + if (component_parent_iface && + component_parent_iface->get_extents) + component_parent_iface->get_extents (component, + x, + y, + &fake_width, + &fake_height, + coord_type); + + g_object_get ( + item, + "text_width", &real_width, + "text_height", &real_height, + NULL); + + if (width) + *width = real_width; + if (height) + *height = real_height; +} + +static const gchar * +et_get_full_text (AtkText *text) +{ + GObject *obj; + EText *etext; + ETextModel *model; + const gchar *full_text; + + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return ""; + + etext = E_TEXT (obj); + g_object_get (etext, "model", &model, NULL); + + full_text = e_text_model_get_text (model); + + return full_text; +} + +static void +et_set_full_text (AtkEditableText *text, + const gchar *full_text) +{ + GObject *obj; + EText *etext; + ETextModel *model; + + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return; + + etext = E_TEXT (obj); + g_object_get (etext, "model", &model, NULL); + + e_text_model_set_text (model, full_text); +} + +static gchar * +et_get_text (AtkText *text, + gint start_offset, + gint end_offset) +{ + gint start, end, real_start, real_end, len; + const gchar *full_text = et_get_full_text (text); + if (full_text == NULL) + return NULL; + len = g_utf8_strlen (full_text, -1); + + start = MIN (MAX (0, start_offset), len); + end = MIN (MAX (-1, end_offset), len); + + if (end_offset == -1) + end = strlen (full_text); + else + end = g_utf8_offset_to_pointer (full_text, end) - full_text; + + start = g_utf8_offset_to_pointer (full_text, start) - full_text; + + real_start = MIN (start, end); + real_end = MAX (start, end); + + return g_strndup (full_text + real_start, real_end - real_start); +} + +static gboolean +is_a_seperator (gunichar c) +{ + return g_unichar_ispunct (c) || g_unichar_isspace (c); +} + +static gint +find_word_start (const gchar *text, + gint begin_offset, + gint step) +{ + gint offset; + gchar *at_offset; + gunichar current, previous; + gint len; + + offset = begin_offset; + len = g_utf8_strlen (text, -1); + + while (offset > 0 && offset < len) { + at_offset = g_utf8_offset_to_pointer (text, offset); + current = g_utf8_get_char_validated (at_offset, -1); + at_offset = g_utf8_offset_to_pointer (text, offset - 1); + previous = g_utf8_get_char_validated (at_offset, -1); + if ((!is_a_seperator (current)) && is_a_seperator (previous)) + break; + offset += step; + } + + return offset; +} + +static gint +find_word_end (const gchar *text, + gint begin_offset, + gint step) +{ + gint offset; + gchar *at_offset; + gunichar current, previous; + gint len; + + offset = begin_offset; + len = g_utf8_strlen (text, -1); + + while (offset > 0 && offset < len) { + at_offset = g_utf8_offset_to_pointer (text, offset); + current = g_utf8_get_char_validated (at_offset, -1); + at_offset = g_utf8_offset_to_pointer (text, offset - 1); + previous = g_utf8_get_char_validated (at_offset, -1); + if (is_a_seperator (current) && (!is_a_seperator (previous))) + break; + offset += step; + } + + return offset; +} + +static gint +find_sentence_start (const gchar *text, + gint begin_offset, + gint step) +{ + gint offset, last_word_end, len; + gchar *at_offset; + gunichar ch; + gint i; + + offset = find_word_start (text, begin_offset, step); + len = g_utf8_strlen (text, -1); + + while (offset > 0 && offset <len) { + last_word_end = find_word_end (text, offset - 1, -1); + if (last_word_end == 0) + break; + for (i = last_word_end; i < offset; i++) { + at_offset = g_utf8_offset_to_pointer (text, i); + ch = g_utf8_get_char_validated (at_offset, -1); + if (ch == '.' || ch == '!' || ch == '?') + return offset; + } + + offset = find_word_start (text, offset + step, step); + } + + return offset; +} + +static gint +find_sentence_end (const gchar *text, + gint begin_offset, + gint step) +{ + gint offset; + gchar *at_offset; + gunichar previous; + gint len; + + offset = begin_offset; + len = g_utf8_strlen (text, -1); + + while (offset > 0 && offset < len) { + at_offset = g_utf8_offset_to_pointer (text, offset - 1); + previous = g_utf8_get_char_validated (at_offset, -1); + if (previous == '.' || previous == '!' || previous == '?') + break; + offset += step; + } + + return offset; +} + +static gint +find_line_start (const gchar *text, + gint begin_offset, + gint step) +{ + gint offset; + gchar *at_offset; + gunichar previous; + gint len; + + offset = begin_offset; + len = g_utf8_strlen (text, -1); + + while (offset > 0 && offset < len) { + at_offset = g_utf8_offset_to_pointer (text, offset - 1); + previous = g_utf8_get_char_validated (at_offset, -1); + if (previous == '\n' || previous == '\r') + break; + offset += step; + } + + return offset; +} + +static gint +find_line_end (const gchar *text, + gint begin_offset, + gint step) +{ + gint offset; + gchar *at_offset; + gunichar current; + gint len; + + offset = begin_offset; + len = g_utf8_strlen (text, -1); + + while (offset >= 0 && offset < len) { + at_offset = g_utf8_offset_to_pointer (text, offset); + current = g_utf8_get_char_validated (at_offset, -1); + if (current == '\n' || current == '\r') + break; + offset += step; + } + + return offset; +} + +static gchar * +et_get_text_after_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + gint start, end, len; + const gchar *full_text = et_get_full_text (text); + g_return_val_if_fail (full_text, NULL); + + switch (boundary_type) + { + case ATK_TEXT_BOUNDARY_CHAR: + start = offset + 1; + end = offset + 2; + break; + case ATK_TEXT_BOUNDARY_WORD_START: + start = find_word_start (full_text, offset + 1, 1); + end = find_word_start (full_text, start + 1, 1); + break; + case ATK_TEXT_BOUNDARY_WORD_END: + start = find_word_end (full_text, offset + 1, 1); + end = find_word_end (full_text, start + 1, 1); + break; + case ATK_TEXT_BOUNDARY_SENTENCE_START: + start = find_sentence_start (full_text, offset + 1, 1); + end = find_sentence_start (full_text, start + 1, 1); + break; + case ATK_TEXT_BOUNDARY_SENTENCE_END: + start = find_sentence_end (full_text, offset + 1, 1); + end = find_sentence_end (full_text, start + 1, 1); + break; + case ATK_TEXT_BOUNDARY_LINE_START: + start = find_line_start (full_text, offset + 1, 1); + end = find_line_start (full_text, start + 1, 1); + break; + case ATK_TEXT_BOUNDARY_LINE_END: + start = find_line_end (full_text, offset + 1, 1); + end = find_line_end (full_text, start + 1, 1); + break; + default: + return NULL; + } + + len = g_utf8_strlen (full_text, -1); + if (start_offset) + *start_offset = MIN (MAX (0, start), len); + if (end_offset) + *end_offset = MIN (MAX (0, end), len); + return et_get_text (text, start, end); +} + +static gchar * +et_get_text_at_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + gint start, end, len; + const gchar *full_text = et_get_full_text (text); + g_return_val_if_fail (full_text, NULL); + + switch (boundary_type) + { + case ATK_TEXT_BOUNDARY_CHAR: + start = offset; + end = offset + 1; + break; + case ATK_TEXT_BOUNDARY_WORD_START: + start = find_word_start (full_text, offset - 1, -1); + end = find_word_start (full_text, offset, 1); + break; + case ATK_TEXT_BOUNDARY_WORD_END: + start = find_word_end (full_text, offset, -1); + end = find_word_end (full_text, offset + 1, 1); + break; + case ATK_TEXT_BOUNDARY_SENTENCE_START: + start = find_sentence_start (full_text, offset - 1, -1); + end = find_sentence_start (full_text, offset, 1); + break; + case ATK_TEXT_BOUNDARY_SENTENCE_END: + start = find_sentence_end (full_text, offset, -1); + end = find_sentence_end (full_text, offset + 1, 1); + break; + case ATK_TEXT_BOUNDARY_LINE_START: + start = find_line_start (full_text, offset - 1, -1); + end = find_line_start (full_text, offset, 1); + break; + case ATK_TEXT_BOUNDARY_LINE_END: + start = find_line_end (full_text, offset, -1); + end = find_line_end (full_text, offset + 1, 1); + break; + default: + return NULL; + } + + len = g_utf8_strlen (full_text, -1); + if (start_offset) + *start_offset = MIN (MAX (0, start), len); + if (end_offset) + *end_offset = MIN (MAX (0, end), len); + return et_get_text (text, start, end); +} + +static gunichar +et_get_character_at_offset (AtkText *text, + gint offset) +{ + const gchar *full_text = et_get_full_text (text); + gchar *at_offset; + + at_offset = g_utf8_offset_to_pointer (full_text, offset); + return g_utf8_get_char_validated (at_offset, -1); +} + +static gchar * +et_get_text_before_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + gint start, end, len; + const gchar *full_text = et_get_full_text (text); + g_return_val_if_fail (full_text, NULL); + + switch (boundary_type) + { + case ATK_TEXT_BOUNDARY_CHAR: + start = offset - 1; + end = offset; + break; + case ATK_TEXT_BOUNDARY_WORD_START: + end = find_word_start (full_text, offset - 1, -1); + start = find_word_start (full_text, end - 1, -1); + break; + case ATK_TEXT_BOUNDARY_WORD_END: + end = find_word_end (full_text, offset, -1); + start = find_word_end (full_text, end - 1, -1); + break; + case ATK_TEXT_BOUNDARY_SENTENCE_START: + end = find_sentence_start (full_text, offset, -1); + start = find_sentence_start (full_text, end - 1, -1); + break; + case ATK_TEXT_BOUNDARY_SENTENCE_END: + end = find_sentence_end (full_text, offset, -1); + start = find_sentence_end (full_text, end - 1, -1); + break; + case ATK_TEXT_BOUNDARY_LINE_START: + end = find_line_start (full_text, offset, -1); + start = find_line_start (full_text, end - 1, -1); + break; + case ATK_TEXT_BOUNDARY_LINE_END: + end = find_line_end (full_text, offset, -1); + start = find_line_end (full_text, end - 1, -1); + break; + default: + return NULL; + } + + len = g_utf8_strlen (full_text, -1); + if (start_offset) + *start_offset = MIN (MAX (0, start), len); + if (end_offset) + *end_offset = MIN (MAX (0, end), len); + return et_get_text (text, start, end); +} + +static gint +et_get_caret_offset (AtkText *text) +{ + GObject *obj; + EText *etext; + gint offset; + + g_return_val_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text), -1); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return -1; + + g_return_val_if_fail (E_IS_TEXT (obj), -1); + etext = E_TEXT (obj); + + g_object_get (etext, "cursor_pos", &offset, NULL); + return offset; +} + +static AtkAttributeSet * +et_get_run_attributes (AtkText *text, + gint offset, + gint *start_offset, + gint *end_offset) +{ + /* Unimplemented */ + return NULL; +} + +static AtkAttributeSet * +et_get_default_attributes (AtkText *text) +{ + /* Unimplemented */ + return NULL; +} + +static void +et_get_character_extents (AtkText *text, + gint offset, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coords) +{ + GObject *obj; + EText *etext; + GnomeCanvas *canvas; + gint x_widget, y_widget, x_window, y_window; + GdkWindow *window; + GtkWidget *widget; + PangoRectangle pango_pos; + + g_return_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text)); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return; + g_return_if_fail (E_IS_TEXT (obj)); + etext = E_TEXT (obj); + canvas = GNOME_CANVAS_ITEM (etext)->canvas; + widget = GTK_WIDGET (canvas); + window = gtk_widget_get_window (widget); + gdk_window_get_origin (window, &x_widget, &y_widget); + + pango_layout_index_to_pos (etext->layout, offset, &pango_pos); + pango_pos.x = PANGO_PIXELS (pango_pos.x); + pango_pos.y = PANGO_PIXELS (pango_pos.y); + pango_pos.width = (pango_pos.width + PANGO_SCALE / 2) / PANGO_SCALE; + pango_pos.height = (pango_pos.height + PANGO_SCALE / 2) / PANGO_SCALE; + + *x = pango_pos.x + x_widget; + *y = pango_pos.y + y_widget; + + *width = pango_pos.width; + *height = pango_pos.height; + + *x += etext->xofs; + *y += etext->yofs; + + if (etext->editing) { + *x -= etext->xofs_edit; + *y -= etext->yofs_edit; + } + + *x += etext->cx; + *y += etext->cy; + + if (coords == ATK_XY_WINDOW) { + window = gdk_window_get_toplevel (window); + gdk_window_get_origin (window, &x_window, &y_window); + *x -= x_window; + *y -= y_window; + } + else if (coords == ATK_XY_SCREEN) { + } + else { + *x = 0; + *y = 0; + *height = 0; + *width = 0; + } +} + +static gint +et_get_character_count (AtkText *text) +{ + const gchar *full_text = et_get_full_text (text); + + return g_utf8_strlen (full_text, -1); +} + +static gint +et_get_offset_at_point (AtkText *text, + gint x, + gint y, + AtkCoordType coords) +{ + GObject *obj; + EText *etext; + GnomeCanvas *canvas; + gint x_widget, y_widget, x_window, y_window; + GdkWindow *window; + GtkWidget *widget; + gint index; + gint trailing; + + g_return_val_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text), -1); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return -1; + g_return_val_if_fail (E_IS_TEXT (obj), -1); + etext = E_TEXT (obj); + canvas = GNOME_CANVAS_ITEM (etext)->canvas; + widget = GTK_WIDGET (canvas); + window = gtk_widget_get_window (widget); + gdk_window_get_origin (window, &x_widget, &y_widget); + + if (coords == ATK_XY_SCREEN) { + x = x - x_widget; + y = y - y_widget; + } + else if (coords == ATK_XY_WINDOW) { + window = gdk_window_get_toplevel (window); + gdk_window_get_origin (window, &x_window, &y_window); + x = x - x_widget + x_window; + y = y - y_widget + y_window; + } + else + return -1; + + x -= etext->xofs; + y -= etext->yofs; + + if (etext->editing) { + x += etext->xofs_edit; + y += etext->yofs_edit; + } + + x -= etext->cx; + y -= etext->cy; + + pango_layout_xy_to_index ( + etext->layout, + x * PANGO_SCALE - PANGO_SCALE / 2, + y * PANGO_SCALE - PANGO_SCALE / 2, + &index, + &trailing); + + return g_utf8_pointer_to_offset (etext->text, etext->text + index + trailing); +} + +static gint +et_get_n_selections (AtkText *text) +{ + EText *etext; + GObject *obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + + if (obj == NULL) + return -1; + etext = E_TEXT (obj); + + if (etext->selection_start != + etext->selection_end) + return 1; + return 0; +} + +static gchar * +et_get_selection (AtkText *text, + gint selection_num, + gint *start_offset, + gint *end_offset) +{ + gint start, end, real_start, real_end, len; + EText *etext; + if (selection_num == 0) { + const gchar *full_text = et_get_full_text (text); + if (full_text == NULL) + return NULL; + len = g_utf8_strlen (full_text, -1); + etext = E_TEXT (atk_gobject_accessible_get_object ( + ATK_GOBJECT_ACCESSIBLE (text))); + start = MIN (etext->selection_start, etext->selection_end); + end = MAX (etext->selection_start, etext->selection_end); + start = MIN (MAX (0, start), len); + end = MIN (MAX (0, end), len); + if (start != end) { + if (start_offset) + *start_offset = start; + if (end_offset) + *end_offset = end; + real_start = g_utf8_offset_to_pointer (full_text, start) - full_text; + real_end = g_utf8_offset_to_pointer (full_text, end) - full_text; + return g_strndup (full_text + real_start, real_end - real_start); + } + } + + return NULL; +} + +static gboolean +et_add_selection (AtkText *text, + gint start_offset, + gint end_offset) +{ + GObject *obj; + EText *etext; + + g_return_val_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text), FALSE); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return FALSE; + g_return_val_if_fail (E_IS_TEXT (obj), FALSE); + etext = E_TEXT (obj); + + g_return_val_if_fail (start_offset >= 0, FALSE); + g_return_val_if_fail (start_offset >= -1, FALSE); + if (end_offset == -1) + end_offset = et_get_character_count (text); + + if (start_offset != end_offset) { + gint real_start, real_end; + real_start = MIN (start_offset, end_offset); + real_end = MAX (start_offset, end_offset); + etext->selection_start = real_start; + etext->selection_end = real_end; + + gnome_canvas_item_grab_focus (GNOME_CANVAS_ITEM (etext)); + gnome_canvas_item_request_update (GNOME_CANVAS_ITEM (etext)); + + g_signal_emit_by_name (ATK_OBJECT (text), "text_selection_changed"); + + return TRUE; + } + + return FALSE; +} + +static gboolean +et_remove_selection (AtkText *text, + gint selection_num) +{ + GObject *obj; + EText *etext; + + g_return_val_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text), FALSE); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return FALSE; + g_return_val_if_fail (E_IS_TEXT (obj), FALSE); + etext = E_TEXT (obj); + + if (selection_num == 0 + && etext->selection_start != etext->selection_end) { + etext->selection_end = etext->selection_start; + g_signal_emit_by_name (ATK_OBJECT (text), "text_selection_changed"); + return TRUE; + } + + return FALSE; +} + +static gboolean +et_set_selection (AtkText *text, + gint selection_num, + gint start_offset, + gint end_offset) +{ + GObject *obj; + + g_return_val_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text), FALSE); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return FALSE; + g_return_val_if_fail (E_IS_TEXT (obj), FALSE); + if (selection_num == 0) + return et_add_selection (text, start_offset, end_offset); + return FALSE; +} + +static gboolean +et_set_caret_offset (AtkText *text, + gint offset) +{ + GObject *obj; + EText *etext; + + g_return_val_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text), FALSE); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return FALSE; + + g_return_val_if_fail (E_IS_TEXT (obj), FALSE); + etext = E_TEXT (obj); + + if (offset < -1) + return FALSE; + else { + ETextEventProcessorCommand command; + + if (offset == -1) + offset = et_get_character_count (text); + + command.action = E_TEP_MOVE; + command.position = E_TEP_VALUE; + command.value = offset; + command.time = GDK_CURRENT_TIME; + g_signal_emit_by_name (etext->tep, "command", &command); + return TRUE; + } +} + +static gboolean +et_set_run_attributes (AtkEditableText *text, + AtkAttributeSet *attrib_set, + gint start_offset, + gint end_offset) +{ + /* Unimplemented */ + return FALSE; +} + +static void +et_set_text_contents (AtkEditableText *text, + const gchar *string) +{ + et_set_full_text (text, string); +} + +static void +et_insert_text (AtkEditableText *text, + const gchar *string, + gint length, + gint *position) +{ + /* Utf8 unimplemented */ + gchar *result; + + const gchar *full_text = et_get_full_text (ATK_TEXT (text)); + if (full_text == NULL) + return; + + result = g_strdup_printf ( + "%.*s%.*s%s", *position, full_text, + length, string, full_text + *position); + + et_set_full_text (text, result); + + *position += length; + + g_free (result); +} + +static void +et_copy_text (AtkEditableText *text, + gint start_pos, + gint end_pos) +{ + GObject *obj; + EText *etext; + + g_return_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text)); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return; + + g_return_if_fail (E_IS_TEXT (obj)); + etext = E_TEXT (obj); + + if (start_pos != end_pos) { + etext->selection_start = start_pos; + etext->selection_end = end_pos; + e_text_copy_clipboard (etext); + } +} + +static void +et_delete_text (AtkEditableText *text, + gint start_pos, + gint end_pos) +{ + GObject *obj; + EText *etext; + + g_return_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text)); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return; + + g_return_if_fail (E_IS_TEXT (obj)); + etext = E_TEXT (obj); + + etext->selection_start = start_pos; + etext->selection_end = end_pos; + + e_text_delete_selection (etext); +} + +static void +et_cut_text (AtkEditableText *text, + gint start_pos, + gint end_pos) +{ + et_copy_text (text, start_pos, end_pos); + et_delete_text (text, start_pos, end_pos); +} + +static void +et_paste_text (AtkEditableText *text, + gint position) +{ + GObject *obj; + EText *etext; + + g_return_if_fail (ATK_IS_GOBJECT_ACCESSIBLE (text)); + obj = atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (text)); + if (obj == NULL) + return; + + g_return_if_fail (E_IS_TEXT (obj)); + etext = E_TEXT (obj); + + g_object_set (etext, "cursor_pos", position, NULL); + e_text_paste_clipboard (etext); +} + +static void +et_atk_component_iface_init (AtkComponentIface *iface) +{ + iface->get_extents = et_get_extents; +} + +static void +et_atk_text_iface_init (AtkTextIface *iface) +{ + iface->get_text = et_get_text; + iface->get_text_after_offset = et_get_text_after_offset; + iface->get_text_at_offset = et_get_text_at_offset; + iface->get_character_at_offset = et_get_character_at_offset; + iface->get_text_before_offset = et_get_text_before_offset; + iface->get_caret_offset = et_get_caret_offset; + iface->get_run_attributes = et_get_run_attributes; + iface->get_default_attributes = et_get_default_attributes; + iface->get_character_extents = et_get_character_extents; + iface->get_character_count = et_get_character_count; + iface->get_offset_at_point = et_get_offset_at_point; + iface->get_n_selections = et_get_n_selections; + iface->get_selection = et_get_selection; + iface->add_selection = et_add_selection; + iface->remove_selection = et_remove_selection; + iface->set_selection = et_set_selection; + iface->set_caret_offset = et_set_caret_offset; +} + +static void +et_atk_editable_text_iface_init (AtkEditableTextIface *iface) +{ + iface->set_run_attributes = et_set_run_attributes; + iface->set_text_contents = et_set_text_contents; + iface->insert_text = et_insert_text; + iface->copy_text = et_copy_text; + iface->cut_text = et_cut_text; + iface->delete_text = et_delete_text; + iface->paste_text = et_paste_text; +} + +static void +_et_reposition_cb (ETextModel *model, + ETextModelReposFn fn, + gpointer repos_data, + gpointer user_data) +{ + AtkObject *accessible; + AtkText *text; + + accessible = ATK_OBJECT (user_data); + text = ATK_TEXT (accessible); + + if (fn == e_repos_delete_shift) { + EReposDeleteShift *info = (EReposDeleteShift *) repos_data; + g_signal_emit_by_name (text, "text-changed::delete", info->pos, info->len); + } + else if (fn == e_repos_insert_shift) { + EReposInsertShift *info = (EReposInsertShift *) repos_data; + g_signal_emit_by_name (text, "text-changed::insert", info->pos, info->len); + } +} + +static void +_et_command_cb (ETextEventProcessor *tep, + ETextEventProcessorCommand *command, + gpointer user_data) +{ + AtkObject *accessible; + AtkText *text; + + accessible = ATK_OBJECT (user_data); + text = ATK_TEXT (accessible); + + switch (command->action) { + case E_TEP_MOVE: + g_signal_emit_by_name (text, "text-caret-moved", et_get_caret_offset (text)); + break; + case E_TEP_SELECT: + g_signal_emit_by_name (text, "text-selection-changed"); + break; + default: + break; + } +} + +static void +et_real_initialize (AtkObject *obj, + gpointer data) +{ + EText *etext; + + ATK_OBJECT_CLASS (parent_class)->initialize (obj, data); + + g_return_if_fail (GAL_A11Y_IS_E_TEXT (obj)); + g_return_if_fail (E_IS_TEXT (data)); + + etext = E_TEXT (data); + + /* Set up signal callbacks */ + g_signal_connect ( + etext->model, "reposition", + G_CALLBACK (_et_reposition_cb), obj); + + if (etext->tep) + g_signal_connect_after ( + etext->tep, "command", + (GCallback) _et_command_cb, obj); + + obj->role = ATK_ROLE_TEXT; +} + +static void +et_class_init (GalA11yETextClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + AtkObjectClass *atk_class = ATK_OBJECT_CLASS (class); + + quark_accessible_object = + g_quark_from_static_string ("gtk-accessible-object"); + parent_class = g_type_class_ref (PARENT_TYPE); + component_parent_iface = + g_type_interface_peek (parent_class, ATK_TYPE_COMPONENT); + object_class->dispose = et_dispose; + atk_class->initialize = et_real_initialize; +} + +static void +et_init (GalA11yEText *a11y) +{ +} + +/** + * gal_a11y_e_text_get_type: + * @void: + * + * Registers the &GalA11yEText class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yEText class. + **/ +GType +gal_a11y_e_text_get_type (void) +{ + static GType type = 0; + + if (!type) { + AtkObjectFactory *factory; + + GTypeInfo info = { + sizeof (GalA11yETextClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) et_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yEText), + 0, + (GInstanceInitFunc) et_init, + NULL /* value_text */ + }; + + static const GInterfaceInfo atk_component_info = { + (GInterfaceInitFunc) et_atk_component_iface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + static const GInterfaceInfo atk_text_info = { + (GInterfaceInitFunc) et_atk_text_iface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + static const GInterfaceInfo atk_editable_text_info = { + (GInterfaceInitFunc) et_atk_editable_text_iface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + factory = atk_registry_get_factory ( + atk_get_default_registry (), GNOME_TYPE_CANVAS_ITEM); + parent_type = atk_object_factory_get_accessible_type (factory); + + type = gal_a11y_type_register_static_with_private ( + PARENT_TYPE, "GalA11yEText", &info, 0, + sizeof (GalA11yETextPrivate), &priv_offset); + + g_type_add_interface_static ( + type, ATK_TYPE_COMPONENT, &atk_component_info); + g_type_add_interface_static ( + type, ATK_TYPE_TEXT, &atk_text_info); + g_type_add_interface_static ( + type, ATK_TYPE_EDITABLE_TEXT, &atk_editable_text_info); + } + + return type; +} + +void +gal_a11y_e_text_init (void) +{ + if (atk_get_root ()) + atk_registry_set_factory_type ( + atk_get_default_registry (), + E_TYPE_TEXT, + gal_a11y_e_text_factory_get_type ()); + +} + diff --git a/e-util/gal-a11y-e-text.h b/e-util/gal-a11y-e-text.h new file mode 100644 index 0000000000..22ebe09dd1 --- /dev/null +++ b/e-util/gal-a11y-e-text.h @@ -0,0 +1,59 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TEXT_H__ +#define __GAL_A11Y_E_TEXT_H__ + +#include <atk/atk.h> + +#define GAL_A11Y_TYPE_E_TEXT (gal_a11y_e_text_get_type ()) +#define GAL_A11Y_E_TEXT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TEXT, GalA11yEText)) +#define GAL_A11Y_E_TEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TEXT, GalA11yETextClass)) +#define GAL_A11Y_IS_E_TEXT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TEXT)) +#define GAL_A11Y_IS_E_TEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TEXT)) + +typedef struct _GalA11yEText GalA11yEText; +typedef struct _GalA11yETextClass GalA11yETextClass; +typedef struct _GalA11yETextPrivate GalA11yETextPrivate; + +/* This struct should actually be larger as this isn't what we derive from. + * The GalA11yETextPrivate comes right after the parent class structure. + **/ +struct _GalA11yEText { + AtkObject object; +}; + +struct _GalA11yETextClass { + AtkObject parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_text_get_type (void); + +void gal_a11y_e_text_init (void); + +#endif /* __GAL_A11Y_E_TEXT_H__ */ diff --git a/e-util/gal-a11y-e-tree-factory.c b/e-util/gal-a11y-e-tree-factory.c new file mode 100644 index 0000000000..00ce55c8c0 --- /dev/null +++ b/e-util/gal-a11y-e-tree-factory.c @@ -0,0 +1,99 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yuedong Du <yuedong.du@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-tree.h" +#include "gal-a11y-e-tree-factory.h" + +static AtkObjectFactoryClass *parent_class; +#define PARENT_TYPE (ATK_TYPE_OBJECT_FACTORY) + +/* Static functions */ + +static GType +gal_a11y_e_tree_factory_get_accessible_type (void) +{ + return GAL_A11Y_TYPE_E_TREE; +} + +static AtkObject * +gal_a11y_e_tree_factory_create_accessible (GObject *obj) +{ + AtkObject *accessible; + + accessible = gal_a11y_e_tree_new (obj); + + return accessible; +} + +static void +gal_a11y_e_tree_factory_class_init (GalA11yETreeFactoryClass *class) +{ + AtkObjectFactoryClass *factory_class = ATK_OBJECT_FACTORY_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + factory_class->create_accessible = gal_a11y_e_tree_factory_create_accessible; + factory_class->get_accessible_type = gal_a11y_e_tree_factory_get_accessible_type; +} + +static void +gal_a11y_e_tree_factory_init (GalA11yETreeFactory *factory) +{ +} + +/** + * gal_a11y_e_tree_factory_get_type: + * @void: + * + * Registers the &GalA11yETreeFactory class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yETreeFactory class. + **/ +GType +gal_a11y_e_tree_factory_get_type (void) +{ + static GType type = 0; + + if (!type) { + GTypeInfo info = { + sizeof (GalA11yETreeFactoryClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) gal_a11y_e_tree_factory_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yETreeFactory), + 0, + (GInstanceInitFunc) gal_a11y_e_tree_factory_init, + NULL /* value_tree */ + }; + + type = g_type_register_static (PARENT_TYPE, "GalA11yETreeFactory", &info, 0); + } + + return type; +} diff --git a/e-util/gal-a11y-e-tree-factory.h b/e-util/gal-a11y-e-tree-factory.h new file mode 100644 index 0000000000..5919ab2091 --- /dev/null +++ b/e-util/gal-a11y-e-tree-factory.h @@ -0,0 +1,52 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yuedong Du <yuedong.du@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TREE_FACTORY_H__ +#define __GAL_A11Y_E_TREE_FACTORY_H__ + +#include <atk/atkobjectfactory.h> + +#define GAL_A11Y_TYPE_E_TREE_FACTORY (gal_a11y_e_table_factory_get_type ()) +#define GAL_A11Y_E_TREE_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TREE_FACTORY, GalA11yETreeFactory)) +#define GAL_A11Y_E_TREE_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TREE_FACTORY, GalA11yETreeFactoryClass)) +#define GAL_A11Y_IS_E_TREE_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TREE_FACTORY)) +#define GAL_A11Y_IS_E_TREE_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TREE_FACTORY)) + +typedef struct _GalA11yETreeFactory GalA11yETreeFactory; +typedef struct _GalA11yETreeFactoryClass GalA11yETreeFactoryClass; + +struct _GalA11yETreeFactory { + AtkObject object; +}; + +struct _GalA11yETreeFactoryClass { + AtkObjectClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_tree_factory_get_type (void); + +#endif /* __GAL_A11Y_E_TREE_FACTORY_H__ */ diff --git a/e-util/gal-a11y-e-tree.c b/e-util/gal-a11y-e-tree.c new file mode 100644 index 0000000000..52c34f312b --- /dev/null +++ b/e-util/gal-a11y-e-tree.c @@ -0,0 +1,196 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yuedong Du <yuedong.du@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-e-tree.h" + +#include "e-table-item.h" +#include "e-tree.h" +#include "gal-a11y-e-table-item.h" +#include "gal-a11y-e-tree-factory.h" +#include "gal-a11y-util.h" + +#define CS_CLASS(a11y) (G_TYPE_INSTANCE_GET_CLASS ((a11y), C_TYPE_STREAM, GalA11yETreeClass)) +static AtkObjectClass *parent_class; +static GType parent_type; +static gint priv_offset; +#define GET_PRIVATE(object) ((GalA11yETreePrivate *) (((gchar *) object) + priv_offset)) +#define PARENT_TYPE (parent_type) + +struct _GalA11yETreePrivate { + AtkObject *child_item; +}; + +/* Static functions */ + +static void +init_child_item (GalA11yETree *a11y) +{ + GalA11yETreePrivate *priv = GET_PRIVATE (a11y); + ETree *tree; + ETableItem * eti; + + tree = E_TREE (gtk_accessible_get_widget (GTK_ACCESSIBLE (a11y))); + g_return_if_fail (tree); + + eti = e_tree_get_item (tree); + if (priv->child_item == NULL) { + priv->child_item = atk_gobject_accessible_for_object (G_OBJECT (eti)); + } +} + +static AtkObject * +et_ref_accessible_at_point (AtkComponent *component, + gint x, + gint y, + AtkCoordType coord_type) +{ + GalA11yETree *a11y = GAL_A11Y_E_TREE (component); + init_child_item (a11y); + return GET_PRIVATE (a11y)->child_item; +} + +static gint +et_get_n_children (AtkObject *accessible) +{ + return 1; +} + +static AtkObject * +et_ref_child (AtkObject *accessible, + gint i) +{ + GalA11yETree *a11y = GAL_A11Y_E_TREE (accessible); + if (i != 0) + return NULL; + init_child_item (a11y); + g_object_ref (GET_PRIVATE (a11y)->child_item); + return GET_PRIVATE (a11y)->child_item; +} + +static AtkLayer +et_get_layer (AtkComponent *component) +{ + return ATK_LAYER_WIDGET; +} + +static void +et_class_init (GalA11yETreeClass *class) +{ + AtkObjectClass *atk_object_class = ATK_OBJECT_CLASS (class); + + parent_class = g_type_class_ref (PARENT_TYPE); + + atk_object_class->get_n_children = et_get_n_children; + atk_object_class->ref_child = et_ref_child; +} + +static void +et_atk_component_iface_init (AtkComponentIface *iface) +{ + iface->ref_accessible_at_point = et_ref_accessible_at_point; + iface->get_layer = et_get_layer; +} + +static void +et_init (GalA11yETree *a11y) +{ + GalA11yETreePrivate *priv; + + priv = GET_PRIVATE (a11y); + + priv->child_item = NULL; +} + +/** + * gal_a11y_e_tree_get_type: + * @void: + * + * Registers the &GalA11yETree class if necessary, and returns the type ID + * associated to it. + * + * Return value: The type ID of the &GalA11yETree class. + **/ +GType +gal_a11y_e_tree_get_type (void) +{ + static GType type = 0; + + if (!type) { + AtkObjectFactory *factory; + + GTypeInfo info = { + sizeof (GalA11yETreeClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) et_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (GalA11yETree), + 0, + (GInstanceInitFunc) et_init, + NULL /* value_tree */ + }; + + static const GInterfaceInfo atk_component_info = { + (GInterfaceInitFunc) et_atk_component_iface_init, + (GInterfaceFinalizeFunc) NULL, + NULL + }; + + factory = atk_registry_get_factory (atk_get_default_registry (), GTK_TYPE_WIDGET); + parent_type = atk_object_factory_get_accessible_type (factory); + + type = gal_a11y_type_register_static_with_private ( + PARENT_TYPE, "GalA11yETree", &info, 0, + sizeof (GalA11yETreePrivate), &priv_offset); + g_type_add_interface_static (type, ATK_TYPE_COMPONENT, &atk_component_info); + } + + return type; +} + +AtkObject * +gal_a11y_e_tree_new (GObject *widget) +{ + GalA11yETree *a11y; + + a11y = g_object_new (gal_a11y_e_tree_get_type (), NULL); + + gtk_accessible_set_widget (GTK_ACCESSIBLE (a11y), GTK_WIDGET (widget)); + + return ATK_OBJECT (a11y); +} + +void +gal_a11y_e_tree_init (void) +{ + if (atk_get_root ()) + atk_registry_set_factory_type ( + atk_get_default_registry (), + E_TYPE_TREE, + gal_a11y_e_tree_factory_get_type ()); +} + diff --git a/e-util/gal-a11y-e-tree.h b/e-util/gal-a11y-e-tree.h new file mode 100644 index 0000000000..709fce0380 --- /dev/null +++ b/e-util/gal-a11y-e-tree.h @@ -0,0 +1,61 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Yuedong Du <yuedong.du@sun.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_E_TREE_H__ +#define __GAL_A11Y_E_TREE_H__ + +#include <gtk/gtk.h> +#include <atk/atkobject.h> +#include <atk/atkcomponent.h> + +#define GAL_A11Y_TYPE_E_TREE (gal_a11y_e_tree_get_type ()) +#define GAL_A11Y_E_TREE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_A11Y_TYPE_E_TREE, GalA11yETree)) +#define GAL_A11Y_E_TREE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_A11Y_TYPE_E_TREE, GalA11yETreeClass)) +#define GAL_A11Y_IS_E_TREE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_A11Y_TYPE_E_TREE)) +#define GAL_A11Y_IS_E_TREE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GAL_A11Y_TYPE_E_TREE)) + +typedef struct _GalA11yETree GalA11yETree; +typedef struct _GalA11yETreeClass GalA11yETreeClass; +typedef struct _GalA11yETreePrivate GalA11yETreePrivate; + +/* This struct should actually be larger as this isn't what we derive from. + * The GalA11yETablePrivate comes right after the parent class structure. + **/ +struct _GalA11yETree { + GtkAccessible object; +}; + +struct _GalA11yETreeClass { + GtkAccessibleClass parent_class; +}; + +/* Standard Glib function */ +GType gal_a11y_e_tree_get_type (void); +AtkObject *gal_a11y_e_tree_new (GObject *tree); + +void gal_a11y_e_tree_init (void); + +#endif /* __GAL_A11Y_E_TREE_H__ */ diff --git a/e-util/gal-a11y-factory.h b/e-util/gal-a11y-factory.h new file mode 100644 index 0000000000..79ffcf286f --- /dev/null +++ b/e-util/gal-a11y-factory.h @@ -0,0 +1,89 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Gilbert Fang <gilbert.fang@sun.com> + * + * This file is mainly from the gailfactory.h of GAIL. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _GAL_A11Y_FACTORY_H__ +#define _GAL_A11Y_FACTORY_H__ + +#include <atk/atkobject.h> +#include <atk/atkobjectfactory.h> + +#define GAL_A11Y_FACTORY(type, type_as_function, opt_create_accessible) \ + \ +static GType \ +type_as_function ## _factory_get_accessible_type (void) \ +{ \ + return type; \ +} \ + \ +static AtkObject * \ +type_as_function ## _factory_create_accessible (GObject *obj) \ +{ \ + GtkWidget *widget; \ + AtkObject *accessible; \ + \ + g_return_val_if_fail (GTK_IS_WIDGET (obj), NULL); \ + \ + widget = GTK_WIDGET (obj); \ + \ + accessible = opt_create_accessible (widget); \ + \ + return accessible; \ +} \ + \ +static void \ +type_as_function ## _factory_class_init (AtkObjectFactoryClass *klass) \ +{ \ + klass->create_accessible = type_as_function ## _factory_create_accessible; \ + klass->get_accessible_type = type_as_function ## _factory_get_accessible_type;\ +} \ + \ +static GType \ +type_as_function ## _factory_get_type (void) \ +{ \ + static GType t = 0; \ + \ + if (!t) \ + { \ + gchar *name; \ + static const GTypeInfo tinfo = \ + { \ + sizeof (AtkObjectFactoryClass), \ + NULL, NULL, (GClassInitFunc) type_as_function ## _factory_class_init, \ + NULL, NULL, sizeof (AtkObjectFactory), 0, NULL, NULL \ + }; \ + \ + name = g_strconcat (g_type_name (type), "Factory", NULL); \ + t = g_type_register_static ( \ + ATK_TYPE_OBJECT_FACTORY, name, &tinfo, 0); \ + g_free (name); \ + } \ + \ + return t; \ +} + +#endif /* _GAL_A11Y_FACTORY_H__ */ diff --git a/e-util/gal-a11y-util.c b/e-util/gal-a11y-util.c new file mode 100644 index 0000000000..9d44758187 --- /dev/null +++ b/e-util/gal-a11y-util.c @@ -0,0 +1,49 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-a11y-util.h" + +GType +gal_a11y_type_register_static_with_private (GType parent_type, + const gchar *type_name, + GTypeInfo *info, + GTypeFlags flags, + gint priv_size, + gint *priv_offset) +{ + GTypeQuery query; + + g_type_query (parent_type, &query); + + info->class_size = query.class_size; + info->instance_size = query.instance_size + priv_size; + + if (priv_offset) + *priv_offset = query.instance_size; + + return g_type_register_static (parent_type, type_name, info, flags); +} diff --git a/e-util/gal-a11y-util.h b/e-util/gal-a11y-util.h new file mode 100644 index 0000000000..75642621d9 --- /dev/null +++ b/e-util/gal-a11y-util.h @@ -0,0 +1,40 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Christopher James Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_A11Y_UTIL_H__ +#define __GAL_A11Y_UTIL_H__ + +#include <glib-object.h> + +GType gal_a11y_type_register_static_with_private (GType parent_type, + const gchar *type_name, + GTypeInfo *info, + GTypeFlags flags, + gint priv_size, + gint *priv_offset); + +#endif /* __GAL_A11Y_UTIL_H__ */ diff --git a/e-util/gal-define-views-dialog.c b/e-util/gal-define-views-dialog.c new file mode 100644 index 0000000000..4bed5944e1 --- /dev/null +++ b/e-util/gal-define-views-dialog.c @@ -0,0 +1,451 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <glib/gi18n.h> + +#include "e-misc-utils.h" +#include "e-util-private.h" + +#include "gal-define-views-dialog.h" +#include "gal-define-views-model.h" +#include "gal-view-new-dialog.h" + +static void gal_define_views_dialog_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); +static void gal_define_views_dialog_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); +static void gal_define_views_dialog_dispose (GObject *object); + +/* The properties we support */ +enum { + PROP_0, + PROP_COLLECTION +}; + +enum { + COL_GALVIEW_NAME, + COL_GALVIEW_DATA +}; + +typedef struct { + gchar *title; + + GtkTreeView *treeview; + GtkTreeModel *model; + + GalDefineViewsDialog *names; +} GalDefineViewsDialogChild; + +G_DEFINE_TYPE (GalDefineViewsDialog, gal_define_views_dialog, GTK_TYPE_DIALOG) + +static void +gal_define_views_dialog_class_init (GalDefineViewsDialogClass *class) +{ + GObjectClass *object_class; + + object_class = (GObjectClass *) class; + + object_class->set_property = gal_define_views_dialog_set_property; + object_class->get_property = gal_define_views_dialog_get_property; + object_class->dispose = gal_define_views_dialog_dispose; + + g_object_class_install_property ( + object_class, + PROP_COLLECTION, + g_param_spec_object ( + "collection", + "Collection", + NULL, + GAL_VIEW_COLLECTION_TYPE, + G_PARAM_READWRITE)); +} + +/* Button callbacks */ + +static void +gdvd_button_new_dialog_callback (GtkWidget *widget, + gint id, + GalDefineViewsDialog *dialog) +{ + gchar *name; + GtkTreeIter iter; + GalView *view; + GalViewCollectionItem *item; + GalViewFactory *factory; + + switch (id) { + case GTK_RESPONSE_OK: + g_object_get ( + widget, + "name", &name, + "factory", &factory, + NULL); + + if (name && factory) { + g_strchomp (name); + if (*name != '\0') { + view = gal_view_factory_new_view (factory, name); + gal_view_collection_append (dialog->collection, view); + + item = dialog->collection->view_data[dialog->collection->view_count - 1]; + gtk_list_store_append (GTK_LIST_STORE (dialog->model), &iter); + gtk_list_store_set ( + GTK_LIST_STORE (dialog->model), &iter, + COL_GALVIEW_NAME, name, + COL_GALVIEW_DATA, item, + -1); + + if (view && GAL_VIEW_GET_CLASS (view)->edit) + gal_view_edit (view, GTK_WINDOW (dialog)); + g_object_unref (view); + } + } + g_object_unref (factory); + g_free (name); + break; + } + gtk_widget_destroy (widget); +} + +static void +gdvd_button_new_callback (GtkWidget *widget, + GalDefineViewsDialog *dialog) +{ + GtkWidget *view_new_dialog = gal_view_new_dialog_new (dialog->collection); + gtk_window_set_transient_for (GTK_WINDOW (view_new_dialog), GTK_WINDOW (dialog)); + g_signal_connect ( + view_new_dialog, "response", + G_CALLBACK (gdvd_button_new_dialog_callback), dialog); + gtk_widget_show (view_new_dialog); +} + +static void +gdvd_button_modify_callback (GtkWidget *widget, + GalDefineViewsDialog *dialog) +{ + GtkTreeIter iter; + GalViewCollectionItem *item; + + if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (dialog->treeview), + &dialog->model, + &iter)) { + gtk_tree_model_get (dialog->model, &iter, COL_GALVIEW_DATA, &item, -1); + + g_return_if_fail (item && !item->built_in); + + gal_view_edit (item->view, GTK_WINDOW (dialog)); + } +} + +static void +gdvd_button_delete_callback (GtkWidget *widget, + GalDefineViewsDialog *dialog) +{ + gint row; + GtkTreeIter iter; + GtkTreePath *path; + GtkTreeSelection *selection; + GalViewCollectionItem *item; + + selection = gtk_tree_view_get_selection (dialog->treeview); + + if (gtk_tree_selection_get_selected (selection, + &dialog->model, + &iter)) { + gtk_tree_model_get (dialog->model, &iter, COL_GALVIEW_DATA, &item, -1); + + g_return_if_fail (item && !item->built_in); + + for (row = 0; row < dialog->collection->view_count; row++) { + if (item == dialog->collection->view_data[row]) { + gal_view_collection_delete_view (dialog->collection, row); + path = gtk_tree_model_get_path (dialog->model, &iter); + gtk_list_store_remove (GTK_LIST_STORE (dialog->model), &iter); + + if (gtk_tree_path_prev (path)) { + gtk_tree_model_get_iter (dialog->model, &iter, path); + } else { + gtk_tree_model_get_iter_first (dialog->model, &iter); + } + + gtk_tree_selection_select_iter (selection, &iter); + break; + } + } + } +} + +static void +gdvd_selection_changed_callback (GtkTreeSelection *selection, + GalDefineViewsDialog *dialog) +{ + GtkWidget *button; + GtkTreeIter iter; + GalViewCollectionItem *item = NULL; + GalViewClass *gvclass = NULL; + + if (gtk_tree_selection_get_selected (selection, &dialog->model, &iter)) { + gtk_tree_model_get (dialog->model, &iter, COL_GALVIEW_DATA, &item, -1); + + if (item && item->view) + gvclass = GAL_VIEW_GET_CLASS (item->view); + } + + button = e_builder_get_widget (dialog->builder, "button-delete"); + gtk_widget_set_sensitive (GTK_WIDGET (button), item && !item->built_in); + + button = e_builder_get_widget (dialog->builder, "button-modify"); + gtk_widget_set_sensitive (GTK_WIDGET (button), item && !item->built_in && gvclass && gvclass->edit != NULL); +} + +static void +gdvd_connect_signal (GalDefineViewsDialog *dialog, + const gchar *widget_name, + const gchar *signal, + GCallback handler) +{ + GtkWidget *widget; + + widget = e_builder_get_widget (dialog->builder, widget_name); + + if (widget) + g_signal_connect (widget, signal, handler, dialog); +} + +static void +dialog_response (GalDefineViewsDialog *dialog, + gint response_id, + gpointer data) +{ + gal_view_collection_save (dialog->collection); +} + +static void +gal_define_views_dialog_init (GalDefineViewsDialog *dialog) +{ + GtkWidget *content_area; + GtkWidget *parent; + GtkWidget *widget; + GtkTreeSelection *selection; + + dialog->collection = NULL; + + dialog->builder = gtk_builder_new (); + e_load_ui_builder_definition (dialog->builder, "gal-define-views.ui"); + + widget = e_builder_get_widget (dialog->builder, "table-top"); + if (!widget) { + return; + } + + g_object_ref (widget); + + parent = gtk_widget_get_parent (widget); + gtk_container_remove (GTK_CONTAINER (parent), widget); + gtk_window_set_default_size (GTK_WINDOW (dialog), 360, 270); + gtk_container_set_border_width (GTK_CONTAINER (dialog), 6); + gtk_container_set_border_width (GTK_CONTAINER (widget), 6); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_box_pack_start (GTK_BOX (content_area), widget, TRUE, TRUE, 0); + + g_object_unref (widget); + + gtk_dialog_add_buttons ( + GTK_DIALOG (dialog), + GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, + NULL); + + dialog->treeview = GTK_TREE_VIEW (e_builder_get_widget (dialog->builder, "treeview1")); + gtk_tree_view_set_reorderable (GTK_TREE_VIEW (dialog->treeview), FALSE); + gtk_tree_view_set_headers_visible (dialog->treeview, TRUE); + + gtk_window_set_resizable (GTK_WINDOW (dialog), TRUE); + + gdvd_connect_signal (dialog, "button-new", "clicked", G_CALLBACK (gdvd_button_new_callback)); + gdvd_connect_signal (dialog, "button-modify", "clicked", G_CALLBACK (gdvd_button_modify_callback)); + gdvd_connect_signal (dialog, "button-delete", "clicked", G_CALLBACK (gdvd_button_delete_callback)); + g_signal_connect ( + dialog, "response", + G_CALLBACK (dialog_response), NULL); + + selection = gtk_tree_view_get_selection (dialog->treeview); + g_signal_connect ( + selection, "changed", + G_CALLBACK (gdvd_selection_changed_callback), dialog); + gdvd_selection_changed_callback (selection, dialog); + + gtk_widget_show (GTK_WIDGET (dialog)); +} + +static void +gal_define_views_dialog_dispose (GObject *object) +{ + GalDefineViewsDialog *gal_define_views_dialog = GAL_DEFINE_VIEWS_DIALOG (object); + + if (gal_define_views_dialog->builder) + g_object_unref (gal_define_views_dialog->builder); + gal_define_views_dialog->builder = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (gal_define_views_dialog_parent_class)->dispose (object); +} + +static void +gal_define_views_dialog_set_collection (GalDefineViewsDialog *dialog, + GalViewCollection *collection) +{ + gint i; + GtkListStore *store; + GtkCellRenderer *renderer; + dialog->collection = collection; + + store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_POINTER); + + for (i = 0; i < collection->view_count; i++) { + GalViewCollectionItem *item = collection->view_data[i]; + GtkTreeIter iter; + + /* hide built in views */ + /*if (item->built_in == 1) + continue;*/ + + gchar *title = NULL; + title = e_str_without_underscores (item->title); + + gtk_list_store_append (store, &iter); + gtk_list_store_set ( + store, &iter, + COL_GALVIEW_NAME, title, + COL_GALVIEW_DATA, item, + -1); + + g_free (title); + } + + gtk_tree_sortable_set_sort_column_id ( + GTK_TREE_SORTABLE (store), + COL_GALVIEW_NAME, GTK_SORT_ASCENDING); + + /* attaching treeview to model */ + gtk_tree_view_set_model (dialog->treeview, GTK_TREE_MODEL (store)); + gtk_tree_view_set_search_column (dialog->treeview, COL_GALVIEW_NAME); + + dialog->model = GTK_TREE_MODEL (store); + + renderer = gtk_cell_renderer_text_new (); + gtk_tree_view_insert_column_with_attributes ( + dialog->treeview, + COL_GALVIEW_NAME, _("Name"), + renderer, "text", COL_GALVIEW_NAME, + NULL); + + /* set sort column */ + gtk_tree_sortable_set_sort_column_id ( + GTK_TREE_SORTABLE (dialog->model), + COL_GALVIEW_NAME, GTK_SORT_ASCENDING); + + if (dialog->builder) { + GtkWidget *widget = e_builder_get_widget (dialog->builder, "label-views"); + if (widget && GTK_IS_LABEL (widget)) { + if (collection->title) { + gchar *text = g_strdup_printf ( + _("Define Views for %s"), + collection->title); + gtk_label_set_text ( + GTK_LABEL (widget), + text); + gtk_window_set_title (GTK_WINDOW (dialog), text); + g_free (text); + } else { + gtk_label_set_text ( + GTK_LABEL (widget), + _("Define Views")); + gtk_window_set_title ( + GTK_WINDOW (dialog), + _("Define Views")); + } + } + } +} + +/** + * gal_define_views_dialog_new + * + * Returns a new dialog for defining views. + * + * Returns: The GalDefineViewsDialog. + */ +GtkWidget * +gal_define_views_dialog_new (GalViewCollection *collection) +{ + GtkWidget *widget = g_object_new (GAL_TYPE_DEFINE_VIEWS_DIALOG, NULL); + gal_define_views_dialog_set_collection (GAL_DEFINE_VIEWS_DIALOG (widget), collection); + return widget; +} + +static void +gal_define_views_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GalDefineViewsDialog *dialog; + + dialog = GAL_DEFINE_VIEWS_DIALOG (object); + + switch (property_id) { + case PROP_COLLECTION: + if (g_value_get_object (value)) + gal_define_views_dialog_set_collection (dialog, GAL_VIEW_COLLECTION (g_value_get_object (value))); + else + gal_define_views_dialog_set_collection (dialog, NULL); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + return; + } +} + +static void +gal_define_views_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GalDefineViewsDialog *dialog; + + dialog = GAL_DEFINE_VIEWS_DIALOG (object); + + switch (property_id) { + case PROP_COLLECTION: + g_value_set_object (value, dialog->collection); + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} diff --git a/e-util/gal-define-views-dialog.h b/e-util/gal-define-views-dialog.h new file mode 100644 index 0000000000..a3b6973cf5 --- /dev/null +++ b/e-util/gal-define-views-dialog.h @@ -0,0 +1,77 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef GAL_DEFINE_VIEWS_DIALOG_H +#define GAL_DEFINE_VIEWS_DIALOG_H + +#include <gtk/gtk.h> +#include <e-util/gal-view-collection.h> + +/* Standard GObject macros */ +#define GAL_TYPE_DEFINE_VIEWS_DIALOG \ + (gal_define_views_dialog_get_type ()) +#define GAL_DEFINE_VIEWS_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), GAL_TYPE_DEFINE_VIEWS_DIALOG, GalDefineViewsDialog)) +#define GAL_DEFINE_VIEWS_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), GAL_TYPE_DEFINE_VIEWS_DIALOG, GalDefineViewsDialogClass)) +#define GAL_IS_DEFINE_VIEWS_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), GAL_TYPE_DEFINE_VIEWS_DIALOG)) +#define GAL_IS_DEFINE_VIEWS_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), GAL_TYPE_DEFINE_VIEWS_DIALOG)) +#define GAL_DEFINE_VIEWS_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), GAL_TYPE_DEFINE_VIEWS_DIALOG, GalDefineViewsDialogClass)) + +G_BEGIN_DECLS + +typedef struct _GalDefineViewsDialog GalDefineViewsDialog; +typedef struct _GalDefineViewsDialogClass GalDefineViewsDialogClass; + +struct _GalDefineViewsDialog { + GtkDialog parent; + + /* item specific fields */ + GtkBuilder *builder; + GtkTreeView *treeview; + GtkTreeModel *model; + + GalViewCollection *collection; +}; + +struct _GalDefineViewsDialogClass { + GtkDialogClass parent_class; +}; + +GType gal_define_views_dialog_get_type (void); +GtkWidget * gal_define_views_dialog_new (GalViewCollection *collection); + +G_END_DECLS + +#endif /* GAL_DEFINE_VIEWS_DIALOG_H */ diff --git a/e-util/gal-define-views-model.c b/e-util/gal-define-views-model.c new file mode 100644 index 0000000000..f9963acbfe --- /dev/null +++ b/e-util/gal-define-views-model.c @@ -0,0 +1,352 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <libxml/tree.h> +#include <libxml/parser.h> +#include <libxml/xmlmemory.h> + +#include <glib/gi18n.h> + +#include "gal-define-views-model.h" + +G_DEFINE_TYPE (GalDefineViewsModel, gal_define_views_model, E_TYPE_TABLE_MODEL) + +enum { + PROP_0, + PROP_EDITABLE, + PROP_COLLECTION +}; + +static void +gal_define_views_model_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GalDefineViewsModel *model; + + model = GAL_DEFINE_VIEWS_MODEL (object); + + switch (property_id) { + case PROP_EDITABLE: + model->editable = g_value_get_boolean (value); + return; + + case PROP_COLLECTION: + e_table_model_pre_change (E_TABLE_MODEL (object)); + if (g_value_get_object (value)) + model->collection = GAL_VIEW_COLLECTION ( + g_value_get_object (value)); + else + model->collection = NULL; + e_table_model_changed (E_TABLE_MODEL (object)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +gal_define_views_model_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GalDefineViewsModel *model; + + model = GAL_DEFINE_VIEWS_MODEL (object); + + switch (property_id) { + case PROP_EDITABLE: + g_value_set_boolean (value, model->editable); + return; + + case PROP_COLLECTION: + g_value_set_object (value, model->collection); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +gdvm_dispose (GObject *object) +{ + GalDefineViewsModel *model = GAL_DEFINE_VIEWS_MODEL (object); + + if (model->collection) + g_object_unref (model->collection); + model->collection = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (gal_define_views_model_parent_class)->dispose (object); +} + +/* This function returns the number of columns in our ETableModel. */ +static gint +gdvm_col_count (ETableModel *etc) +{ + return 1; +} + +/* This function returns the number of rows in our ETableModel. */ +static gint +gdvm_row_count (ETableModel *etc) +{ + GalDefineViewsModel *views = GAL_DEFINE_VIEWS_MODEL (etc); + if (views->collection) + return gal_view_collection_get_count (views->collection); + else + return 0; +} + +/* This function returns the value at a particular point in our ETableModel. */ +static gpointer +gdvm_value_at (ETableModel *etc, + gint col, + gint row) +{ + GalDefineViewsModel *views = GAL_DEFINE_VIEWS_MODEL (etc); + GalView *view; + const gchar *value; + + view = gal_view_collection_get_view (views->collection, row); + value = gal_view_get_title (view); + + return (gpointer) ((value != NULL) ? value : ""); +} + +/* This function sets the value at a particular point in our ETableModel. */ +static void +gdvm_set_value_at (ETableModel *etc, + gint col, + gint row, + gconstpointer val) +{ + GalDefineViewsModel *views = GAL_DEFINE_VIEWS_MODEL (etc); + if (views->editable) { + GalView *view; + + view = gal_view_collection_get_view (views->collection, row); + + e_table_model_pre_change (etc); + gal_view_set_title (view, val); + e_table_model_cell_changed (etc, col, row); + } +} + +/* This function returns whether a particular cell is editable. */ +static gboolean +gdvm_is_cell_editable (ETableModel *etc, + gint col, + gint row) +{ + return GAL_DEFINE_VIEWS_MODEL (etc)->editable; +} + +static void +gdvm_append_row (ETableModel *etm, + ETableModel *source, + gint row) +{ +} + +/* This function duplicates the value passed to it. */ +static gpointer +gdvm_duplicate_value (ETableModel *etc, + gint col, + gconstpointer value) +{ + return g_strdup (value); +} + +/* This function frees the value passed to it. */ +static void +gdvm_free_value (ETableModel *etc, + gint col, + gpointer value) +{ + g_free (value); +} + +static gpointer +gdvm_initialize_value (ETableModel *etc, + gint col) +{ + return g_strdup (""); +} + +static gboolean +gdvm_value_is_empty (ETableModel *etc, + gint col, + gconstpointer value) +{ + return !(value && *(gchar *) value); +} + +static gchar * +gdvm_value_to_string (ETableModel *etc, + gint col, + gconstpointer value) +{ + return g_strdup (value); +} + +/** + * gal_define_views_model_append + * @model: The model to add to. + * @view: The view to add. + * + * Adds the given view to the gal define views model. + */ +void +gal_define_views_model_append (GalDefineViewsModel *model, + GalView *view) +{ + ETableModel *etm = E_TABLE_MODEL (model); + + e_table_model_pre_change (etm); + gal_view_collection_append (model->collection, view); + e_table_model_row_inserted ( + etm, gal_view_collection_get_count (model->collection) - 1); +} + +static void +gal_define_views_model_class_init (GalDefineViewsModelClass *class) +{ + ETableModelClass *model_class = E_TABLE_MODEL_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = gdvm_dispose; + object_class->set_property = gal_define_views_model_set_property; + object_class->get_property = gal_define_views_model_get_property; + + g_object_class_install_property ( + object_class, + PROP_EDITABLE, + g_param_spec_boolean ( + "editable", + "Editable", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_COLLECTION, + g_param_spec_object ( + "collection", + "Collection", + NULL, + GAL_VIEW_COLLECTION_TYPE, + G_PARAM_READWRITE)); + + model_class->column_count = gdvm_col_count; + model_class->row_count = gdvm_row_count; + model_class->value_at = gdvm_value_at; + model_class->set_value_at = gdvm_set_value_at; + model_class->is_cell_editable = gdvm_is_cell_editable; + model_class->append_row = gdvm_append_row; + model_class->duplicate_value = gdvm_duplicate_value; + model_class->free_value = gdvm_free_value; + model_class->initialize_value = gdvm_initialize_value; + model_class->value_is_empty = gdvm_value_is_empty; + model_class->value_to_string = gdvm_value_to_string; +} + +static void +gal_define_views_model_init (GalDefineViewsModel *model) +{ + model->collection = NULL; +} + +/** + * gal_define_views_model_new + * + * Returns a new define views model. This is a list of views as an + * ETable for use in the GalDefineViewsDialog. + * + * Returns: The new GalDefineViewsModel. + */ +ETableModel * +gal_define_views_model_new (void) +{ + GalDefineViewsModel *et; + + et = g_object_new (GAL_DEFINE_VIEWS_MODEL_TYPE, NULL); + + return E_TABLE_MODEL (et); +} + +/** + * gal_define_views_model_get_view: + * @model: The GalDefineViewsModel. + * @n: Which view to get. + * + * Gets the nth view. + * + * Returns: The view. + */ +GalView * +gal_define_views_model_get_view (GalDefineViewsModel *model, + gint n) +{ + return gal_view_collection_get_view (model->collection, n); +} + +/** + * gal_define_views_model_delete_view: + * @model: The GalDefineViewsModel. + * @n: Which view to delete. + * + * Deletes the nth view. + */ +void +gal_define_views_model_delete_view (GalDefineViewsModel *model, + gint n) +{ + e_table_model_pre_change (E_TABLE_MODEL (model)); + gal_view_collection_delete_view (model->collection, n); + e_table_model_row_deleted (E_TABLE_MODEL (model), n); +} + +/** + * gal_define_views_model_copy_view: + * @model: The GalDefineViewsModel. + * @n: Which view to copy. + * + * Copys the nth view. + */ +void +gal_define_views_model_copy_view (GalDefineViewsModel *model, + gint n) +{ + ETableModel *etm = E_TABLE_MODEL (model); + e_table_model_pre_change (etm); + gal_view_collection_copy_view (model->collection, n); + e_table_model_row_inserted ( + etm, gal_view_collection_get_count (model->collection) - 1); +} diff --git a/e-util/gal-define-views-model.h b/e-util/gal-define-views-model.h new file mode 100644 index 0000000000..7219384a59 --- /dev/null +++ b/e-util/gal-define-views-model.h @@ -0,0 +1,70 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _GAL_DEFINE_VIEWS_MODEL_H_ +#define _GAL_DEFINE_VIEWS_MODEL_H_ + +#include <e-util/e-table-model.h> +#include <e-util/gal-view.h> +#include <e-util/gal-view-collection.h> + +G_BEGIN_DECLS + +#define GAL_DEFINE_VIEWS_MODEL_TYPE (gal_define_views_model_get_type ()) +#define GAL_DEFINE_VIEWS_MODEL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GAL_DEFINE_VIEWS_MODEL_TYPE, GalDefineViewsModel)) +#define GAL_DEFINE_VIEWS_MODEL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GAL_DEFINE_VIEWS_MODEL_TYPE, GalDefineViewsModelClass)) +#define GAL_IS_DEFINE_VIEWS_MODEL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GAL_DEFINE_VIEWS_MODEL_TYPE)) +#define GAL_IS_DEFINE_VIEWS_MODEL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GAL_DEFINE_VIEWS_MODEL_TYPE)) + +typedef struct { + ETableModel parent; + + /* item specific fields */ + GalViewCollection *collection; + + guint editable : 1; +} GalDefineViewsModel; + +typedef struct { + ETableModelClass parent_class; +} GalDefineViewsModelClass; + +GType gal_define_views_model_get_type (void); +ETableModel *gal_define_views_model_new (void); + +void gal_define_views_model_append (GalDefineViewsModel *model, + GalView *view); +GalView *gal_define_views_model_get_view (GalDefineViewsModel *model, + gint i); +void gal_define_views_model_delete_view (GalDefineViewsModel *model, + gint i); +void gal_define_views_model_copy_view (GalDefineViewsModel *model, + gint i); + +G_END_DECLS + +#endif /* _GAL_DEFINE_VIEWS_MODEL_H_ */ diff --git a/e-util/gal-define-views.ui b/e-util/gal-define-views.ui new file mode 100644 index 0000000000..b3314aa4ee --- /dev/null +++ b/e-util/gal-define-views.ui @@ -0,0 +1,177 @@ +<?xml version="1.0"?> +<!--*- mode: xml -*--> +<interface> + <object class="GtkDialog" id="dialog1"> + <property name="title" translatable="yes">Define Views for "%s"</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkTable" id="table-top"> + <property name="visible">True</property> + <property name="n_rows">2</property> + <property name="n_columns">1</property> + <property name="column_spacing">6</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkLabel" id="label-views"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Define Views for %s</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="border_width">6</property> + <property name="spacing">6</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="hscrollbar_policy">GTK_POLICY_NEVER</property> + <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <child> + <object class="GtkTreeView" id="treeview1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="headers_clickable">True</property> + <property name="reorderable">True</property> + <property name="fixed_height_mode">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkVButtonBox" id="vbuttonbox1"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="spacing">6</property> + <property name="layout_style">GTK_BUTTONBOX_START</property> + <child> + <object class="GtkButton" id="button-new"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-new</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button-modify"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <child> + <object class="GtkAlignment" id="alignment33"> + <property name="visible">True</property> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <object class="GtkHBox" id="hbox224"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <object class="GtkImage" id="image8"> + <property name="visible">True</property> + <property name="stock">gtk-properties</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label557"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Edit</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button-delete"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-delete</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + </object> + <packing> + <property name="padding">12</property> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">GTK_BUTTONBOX_END</property> + <child> + <object class="GtkButton" id="button7"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="label">gtk-close</property> + <property name="use_stock">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-5">button7</action-widget> + </action-widgets> + </object> +</interface> diff --git a/e-util/gal-view-collection.c b/e-util/gal-view-collection.c new file mode 100644 index 0000000000..bcbad52fea --- /dev/null +++ b/e-util/gal-view-collection.c @@ -0,0 +1,829 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-view-collection.h" + +#include <ctype.h> +#include <string.h> +#include <errno.h> + +#include <libxml/parser.h> +#include <libedataserver/libedataserver.h> + +#include <glib/gi18n.h> + +#include "e-unicode.h" +#include "e-xml-utils.h" + +G_DEFINE_TYPE (GalViewCollection, gal_view_collection, G_TYPE_OBJECT) + +#define d(x) + +enum { + DISPLAY_VIEW, + CHANGED, + LAST_SIGNAL +}; + +static guint gal_view_collection_signals[LAST_SIGNAL] = { 0, }; + +/** + * gal_view_collection_display_view: + * @collection: The GalViewCollection to send the signal on. + * @view: The view to display. + * + */ +void +gal_view_collection_display_view (GalViewCollection *collection, + GalView *view) +{ + g_return_if_fail (GAL_IS_VIEW_COLLECTION (collection)); + g_return_if_fail (GAL_IS_VIEW (view)); + + g_signal_emit ( + collection, + gal_view_collection_signals[DISPLAY_VIEW], 0, + view); +} + +static void +gal_view_collection_changed (GalViewCollection *collection) +{ + g_return_if_fail (GAL_IS_VIEW_COLLECTION (collection)); + + g_signal_emit ( + collection, + gal_view_collection_signals[CHANGED], 0); +} + +static void +gal_view_collection_item_free (GalViewCollectionItem *item) +{ + g_free (item->id); + if (item->view) { + if (item->view_changed_id) + g_signal_handler_disconnect ( + item->view, + item->view_changed_id); + g_object_unref (item->view); + } + g_free (item); +} + +static gchar * +gal_view_generate_string (GalViewCollection *collection, + GalView *view, + gint which) +{ + gchar *ret_val; + gchar *pointer; + + if (which == 1) + ret_val = g_strdup (gal_view_get_title (view)); + else + ret_val = g_strdup_printf ("%s_%d", gal_view_get_title (view), which); + for (pointer = ret_val; *pointer; pointer = g_utf8_next_char (pointer)) { + if (!g_unichar_isalnum (g_utf8_get_char (pointer))) { + gchar *ptr = pointer; + for (; ptr < g_utf8_next_char (pointer); *ptr = '_', ptr++) + ; + } + } + return ret_val; +} + +static gint +gal_view_check_string (GalViewCollection *collection, + gchar *string) +{ + gint i; + + if (!strcmp (string, "current_view")) + return FALSE; + + for (i = 0; i < collection->view_count; i++) { + if (!strcmp (string, collection->view_data[i]->id)) + return FALSE; + } + for (i = 0; i < collection->removed_view_count; i++) { + if (!strcmp (string, collection->removed_view_data[i]->id)) + return FALSE; + } + return TRUE; +} + +static gchar * +gal_view_generate_id (GalViewCollection *collection, + GalView *view) +{ + gint i; + for (i = 1; TRUE; i++) { + gchar *try; + + try = gal_view_generate_string (collection, view, i); + if (gal_view_check_string (collection, try)) + return try; + g_free (try); + } +} + +static void +gal_view_collection_dispose (GObject *object) +{ + GalViewCollection *collection = GAL_VIEW_COLLECTION (object); + gint i; + + for (i = 0; i < collection->view_count; i++) { + gal_view_collection_item_free (collection->view_data[i]); + } + g_free (collection->view_data); + collection->view_data = NULL; + collection->view_count = 0; + + g_list_foreach ( + collection->factory_list, + (GFunc) g_object_unref, NULL); + g_list_free (collection->factory_list); + collection->factory_list = NULL; + + for (i = 0; i < collection->removed_view_count; i++) { + gal_view_collection_item_free (collection->removed_view_data[i]); + } + g_free (collection->removed_view_data); + collection->removed_view_data = NULL; + collection->removed_view_count = 0; + + g_free (collection->system_dir); + collection->system_dir = NULL; + + g_free (collection->local_dir); + collection->local_dir = NULL; + + g_free (collection->default_view); + collection->default_view = NULL; + + g_free (collection->title); + collection->title = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (gal_view_collection_parent_class)->dispose (object); +} + +static void +gal_view_collection_class_init (GalViewCollectionClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = gal_view_collection_dispose; + + gal_view_collection_signals[DISPLAY_VIEW] = g_signal_new ( + "display_view", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GalViewCollectionClass, display_view), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + GAL_TYPE_VIEW); + + gal_view_collection_signals[CHANGED] = g_signal_new ( + "changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GalViewCollectionClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + class->display_view = NULL; + class->changed = NULL; +} + +static void +gal_view_collection_init (GalViewCollection *collection) +{ + collection->view_data = NULL; + collection->view_count = 0; + collection->factory_list = NULL; + + collection->removed_view_data = NULL; + collection->removed_view_count = 0; + + collection->system_dir = NULL; + collection->local_dir = NULL; + + collection->loaded = FALSE; + collection->default_view = NULL; + collection->default_view_built_in = TRUE; + + collection->title = NULL; +} + +/** + * gal_view_collection_new: + * + * A collection of views and view factories. + */ +GalViewCollection * +gal_view_collection_new (void) +{ + return g_object_new (GAL_VIEW_COLLECTION_TYPE, NULL); +} + +void +gal_view_collection_set_title (GalViewCollection *collection, + const gchar *title) +{ + g_free (collection->title); + collection->title = g_strdup (title); +} + +/** + * gal_view_collection_set_storage_directories + * @collection: The view collection to initialize + * @system_dir: The location of the system built in views + * @local_dir: The location to store the users set up views + * + * Sets up the GalViewCollection. + */ +void +gal_view_collection_set_storage_directories (GalViewCollection *collection, + const gchar *system_dir, + const gchar *local_dir) +{ + g_return_if_fail (GAL_IS_VIEW_COLLECTION (collection)); + g_return_if_fail (system_dir != NULL); + g_return_if_fail (local_dir != NULL); + + g_free (collection->system_dir); + g_free (collection->local_dir); + + collection->system_dir = g_strdup (system_dir); + collection->local_dir = g_strdup (local_dir); +} + +/** + * gal_view_collection_add_factory + * @collection: The view collection to add a factory to + * @factory: The factory to add. The @collection will add a reference + * to the factory object, so you should unref it after calling this + * function if you no longer need it. + * + * Adds the given factory to this collection. This list is used both + * when loading views from their xml description as well as when the + * user tries to create a new view. + */ +void +gal_view_collection_add_factory (GalViewCollection *collection, + GalViewFactory *factory) +{ + g_return_if_fail (GAL_IS_VIEW_COLLECTION (collection)); + g_return_if_fail (GAL_IS_VIEW_FACTORY (factory)); + + g_object_ref (factory); + collection->factory_list = g_list_prepend (collection->factory_list, factory); +} + +static void +view_changed (GalView *view, + GalViewCollectionItem *item) +{ + item->changed = TRUE; + item->ever_changed = TRUE; + + g_signal_handler_block (item->view, item->view_changed_id); + gal_view_collection_changed (item->collection); + g_signal_handler_unblock (item->view, item->view_changed_id); +} + +/* Use factory list to load a GalView file. */ +static GalView * +gal_view_collection_real_load_view_from_file (GalViewCollection *collection, + const gchar *type, + const gchar *title, + const gchar *dir, + const gchar *filename) +{ + GalViewFactory *factory; + GList *factories; + + factory = NULL; + for (factories = collection->factory_list; factories; factories = factories->next) { + if (type && !strcmp (gal_view_factory_get_type_code (factories->data), type)) { + factory = factories->data; + break; + } + } + if (factory) { + GalView *view; + + view = gal_view_factory_new_view (factory, title); + gal_view_set_title (view, title); + gal_view_load (view, filename); + return view; + } + return NULL; +} + +GalView * +gal_view_collection_load_view_from_file (GalViewCollection *collection, + const gchar *type, + const gchar *filename) +{ + return gal_view_collection_real_load_view_from_file (collection, type, "", collection->local_dir, filename); +} + +static GalViewCollectionItem * +load_single_file (GalViewCollection *collection, + gchar *dir, + gboolean local, + xmlNode *node) +{ + GalViewCollectionItem *item; + item = g_new (GalViewCollectionItem, 1); + item->ever_changed = local; + item->changed = FALSE; + item->built_in = !local; + item->id = e_xml_get_string_prop_by_name (node, (const guchar *)"id"); + item->filename = e_xml_get_string_prop_by_name (node, (const guchar *)"filename"); + item->title = e_xml_get_translated_utf8_string_prop_by_name (node, (const guchar *)"title"); + item->type = e_xml_get_string_prop_by_name (node, (const guchar *)"type"); + item->collection = collection; + item->view_changed_id = 0; + + if (item->filename) { + gchar *fullpath; + fullpath = g_build_filename (dir, item->filename, NULL); + item->view = gal_view_collection_real_load_view_from_file (collection, item->type, item->title, dir, fullpath); + g_free (fullpath); + if (item->view) { + item->view_changed_id = g_signal_connect ( + item->view, "changed", + G_CALLBACK (view_changed), item); + } + } + return item; +} + +static void +load_single_dir (GalViewCollection *collection, + gchar *dir, + gboolean local) +{ + xmlDoc *doc = NULL; + xmlNode *root; + xmlNode *child; + gchar *filename = g_build_filename (dir, "galview.xml", NULL); + gchar *default_view; + + if (g_file_test (filename, G_FILE_TEST_IS_REGULAR)) { +#ifdef G_OS_WIN32 + gchar *locale_filename = g_win32_locale_filename_from_utf8 (filename); + if (locale_filename != NULL) + doc = xmlParseFile (locale_filename); + g_free (locale_filename); +#else + doc = xmlParseFile (filename); +#endif + } + + if (!doc) { + g_free (filename); + return; + } + root = xmlDocGetRootElement (doc); + for (child = root->xmlChildrenNode; child; child = child->next) { + gchar *id; + gboolean found = FALSE; + gint i; + + if (!strcmp ((gchar *) child->name, "text")) + continue; + + id = e_xml_get_string_prop_by_name (child, (const guchar *)"id"); + for (i = 0; i < collection->view_count; i++) { + if (!strcmp (id, collection->view_data[i]->id)) { + if (!local) + collection->view_data[i]->built_in = TRUE; + found = TRUE; + break; + } + } + if (!found) { + for (i = 0; i < collection->removed_view_count; i++) { + if (!strcmp (id, collection->removed_view_data[i]->id)) { + if (!local) + collection->removed_view_data[i]->built_in = TRUE; + found = TRUE; + break; + } + } + } + + if (!found) { + GalViewCollectionItem *item = load_single_file (collection, dir, local, child); + if (item->filename && *item->filename) { + collection->view_data = g_renew (GalViewCollectionItem *, collection->view_data, collection->view_count + 1); + collection->view_data[collection->view_count] = item; + collection->view_count++; + } else { + collection->removed_view_data = g_renew (GalViewCollectionItem *, collection->removed_view_data, collection->removed_view_count + 1); + collection->removed_view_data[collection->removed_view_count] = item; + collection->removed_view_count++; + } + } + g_free (id); + } + + default_view = e_xml_get_string_prop_by_name (root, (const guchar *)"default-view"); + if (default_view) { + if (local) + collection->default_view_built_in = FALSE; + else + collection->default_view_built_in = TRUE; + g_free (collection->default_view); + collection->default_view = default_view; + } + + g_free (filename); + xmlFreeDoc (doc); +} + +/** + * gal_view_collection_load + * @collection: The view collection to load information for + * + * Loads the data from the system and user directories specified in + * set storage directories. This is primarily for internal use by + * other parts of gal_view. + */ +void +gal_view_collection_load (GalViewCollection *collection) +{ + g_return_if_fail (GAL_IS_VIEW_COLLECTION (collection)); + g_return_if_fail (collection->local_dir != NULL); + g_return_if_fail (collection->system_dir != NULL); + g_return_if_fail (!collection->loaded); + + if ((g_mkdir_with_parents (collection->local_dir, 0777) == -1) && (errno != EEXIST)) + g_warning ("Unable to create dir %s: %s", collection->local_dir, g_strerror (errno)); + + load_single_dir (collection, collection->local_dir, TRUE); + load_single_dir (collection, collection->system_dir, FALSE); + gal_view_collection_changed (collection); + + collection->loaded = TRUE; +} + +/** + * gal_view_collection_save + * @collection: The view collection to save information for + * + * Saves the data to the user directory specified in set storage + * directories. This is primarily for internal use by other parts of + * gal_view. + */ +void +gal_view_collection_save (GalViewCollection *collection) +{ + gint i; + xmlDoc *doc; + xmlNode *root; + gchar *filename; + + g_return_if_fail (GAL_IS_VIEW_COLLECTION (collection)); + g_return_if_fail (collection->local_dir != NULL); + + doc = xmlNewDoc ((const guchar *)"1.0"); + root = xmlNewNode (NULL, (const guchar *)"GalViewCollection"); + xmlDocSetRootElement (doc, root); + + if (collection->default_view && !collection->default_view_built_in) { + e_xml_set_string_prop_by_name (root, (const guchar *)"default-view", collection->default_view); + } + + for (i = 0; i < collection->view_count; i++) { + xmlNode *child; + GalViewCollectionItem *item; + + item = collection->view_data[i]; + if (item->ever_changed) { + child = xmlNewChild (root, NULL, (const guchar *)"GalView", NULL); + e_xml_set_string_prop_by_name (child, (const guchar *)"id", item->id); + e_xml_set_string_prop_by_name (child, (const guchar *)"title", item->title); + e_xml_set_string_prop_by_name (child, (const guchar *)"filename", item->filename); + e_xml_set_string_prop_by_name (child, (const guchar *)"type", item->type); + + if (item->changed) { + filename = g_build_filename (collection->local_dir, item->filename, NULL); + gal_view_save (item->view, filename); + g_free (filename); + } + } + } + for (i = 0; i < collection->removed_view_count; i++) { + xmlNode *child; + GalViewCollectionItem *item; + + item = collection->removed_view_data[i]; + + child = xmlNewChild (root, NULL, (const guchar *)"GalView", NULL); + e_xml_set_string_prop_by_name (child, (const guchar *)"id", item->id); + e_xml_set_string_prop_by_name (child, (const guchar *)"title", item->title); + e_xml_set_string_prop_by_name (child, (const guchar *)"type", item->type); + } + filename = g_build_filename (collection->local_dir, "galview.xml", NULL); + if (e_xml_save_file (filename, doc) == -1) + g_warning ("Unable to save view to %s - %s", filename, g_strerror (errno)); + xmlFreeDoc (doc); + g_free (filename); +} + +/** + * gal_view_collection_get_count + * @collection: The view collection to count + * + * Calculates the number of views in the given collection. + * + * Returns: The number of views in the collection. + */ +gint +gal_view_collection_get_count (GalViewCollection *collection) +{ + g_return_val_if_fail (GAL_IS_VIEW_COLLECTION (collection), -1); + + return collection->view_count; +} + +/** + * gal_view_collection_get_view + * @collection: The view collection to query + * @n: The view to get. + * + * Returns: The nth view in the collection + */ +GalView * +gal_view_collection_get_view (GalViewCollection *collection, + gint n) +{ + g_return_val_if_fail (GAL_IS_VIEW_COLLECTION (collection), NULL); + g_return_val_if_fail (n < collection->view_count, NULL); + g_return_val_if_fail (n >= 0, NULL); + + return collection->view_data[n]->view; +} + +/** + * gal_view_collection_get_view_item + * @collection: The view collection to query + * @n: The view item to get. + * + * Returns: The nth view item in the collection + */ +GalViewCollectionItem * +gal_view_collection_get_view_item (GalViewCollection *collection, + gint n) +{ + g_return_val_if_fail (GAL_IS_VIEW_COLLECTION (collection), NULL); + g_return_val_if_fail (n < collection->view_count, NULL); + g_return_val_if_fail (n >= 0, NULL); + + return collection->view_data[n]; +} + +gint +gal_view_collection_get_view_index_by_id (GalViewCollection *collection, + const gchar *view_id) +{ + gint i; + for (i = 0; i < collection->view_count; i++) { + if (!strcmp (collection->view_data[i]->id, view_id)) + return i; + } + return -1; +} + +gchar * +gal_view_collection_get_view_id_by_index (GalViewCollection *collection, + gint n) +{ + g_return_val_if_fail (GAL_IS_VIEW_COLLECTION (collection), NULL); + g_return_val_if_fail (n < collection->view_count, NULL); + g_return_val_if_fail (n >= 0, NULL); + + return g_strdup (collection->view_data[n]->id); +} + +void +gal_view_collection_append (GalViewCollection *collection, + GalView *view) +{ + GalViewCollectionItem *item; + + g_return_if_fail (GAL_IS_VIEW_COLLECTION (collection)); + g_return_if_fail (GAL_IS_VIEW (view)); + + item = g_new (GalViewCollectionItem, 1); + item->ever_changed = TRUE; + item->changed = TRUE; + item->built_in = FALSE; + item->title = g_strdup (gal_view_get_title (view)); + item->type = g_strdup (gal_view_get_type_code (view)); + item->id = gal_view_generate_id (collection, view); + item->filename = g_strdup_printf ("%s.galview", item->id); + item->view = view; + item->collection = collection; + g_object_ref (view); + + item->view_changed_id = g_signal_connect ( + item->view, "changed", + G_CALLBACK (view_changed), item); + + collection->view_data = g_renew (GalViewCollectionItem *, collection->view_data, collection->view_count + 1); + collection->view_data[collection->view_count] = item; + collection->view_count++; + + gal_view_collection_changed (collection); +} + +void +gal_view_collection_delete_view (GalViewCollection *collection, + gint i) +{ + GalViewCollectionItem *item; + + g_return_if_fail (GAL_IS_VIEW_COLLECTION (collection)); + g_return_if_fail (i >= 0 && i < collection->view_count); + + item = collection->view_data[i]; + memmove (collection->view_data + i, collection->view_data + i + 1, (collection->view_count - i - 1) * sizeof (GalViewCollectionItem *)); + collection->view_count--; + if (item->built_in) { + g_free (item->filename); + item->filename = NULL; + + collection->removed_view_data = g_renew (GalViewCollectionItem *, collection->removed_view_data, collection->removed_view_count + 1); + collection->removed_view_data[collection->removed_view_count] = item; + collection->removed_view_count++; + } else { + gal_view_collection_item_free (item); + } + + gal_view_collection_changed (collection); +} + +void +gal_view_collection_copy_view (GalViewCollection *collection, + gint i) +{ + GalViewCollectionItem *item; + GalView *view; + + g_return_if_fail (GAL_IS_VIEW_COLLECTION (collection)); + g_return_if_fail (i >= 0 && i < collection->view_count); + + view = collection->view_data[i]->view; + + item = g_new (GalViewCollectionItem, 1); + item->ever_changed = TRUE; + item->changed = FALSE; + item->built_in = FALSE; + item->title = g_strdup (gal_view_get_title (view)); + item->type = g_strdup (gal_view_get_type_code (view)); + item->id = gal_view_generate_id (collection, view); + item->filename = g_strdup_printf ("%s.galview", item->id); + item->view = gal_view_clone (view); + item->collection = collection; + + item->view_changed_id = g_signal_connect ( + item->view, "changed", + G_CALLBACK (view_changed), item); + + collection->view_data = g_renew (GalViewCollectionItem *, collection->view_data, collection->view_count + 1); + collection->view_data[collection->view_count] = item; + collection->view_count++; + + gal_view_collection_changed (collection); +} + +gboolean +gal_view_collection_loaded (GalViewCollection *collection) +{ + return collection->loaded; +} + +const gchar * +gal_view_collection_append_with_title (GalViewCollection *collection, + const gchar *title, + GalView *view) +{ + GalViewCollectionItem *item; + + g_return_val_if_fail (GAL_IS_VIEW_COLLECTION (collection), NULL); + g_return_val_if_fail (GAL_IS_VIEW (view), NULL); + + gal_view_set_title (view, title); + + d (g_print ("%s: %p\n", G_STRFUNC, view)); + + item = g_new (GalViewCollectionItem, 1); + item->ever_changed = TRUE; + item->changed = TRUE; + item->built_in = FALSE; + item->title = g_strdup (gal_view_get_title (view)); + item->type = g_strdup (gal_view_get_type_code (view)); + item->id = gal_view_generate_id (collection, view); + item->filename = g_strdup_printf ("%s.galview", item->id); + item->view = view; + item->collection = collection; + g_object_ref (view); + + item->view_changed_id = g_signal_connect ( + item->view, "changed", + G_CALLBACK (view_changed), item); + + collection->view_data = g_renew (GalViewCollectionItem *, collection->view_data, collection->view_count + 1); + collection->view_data[collection->view_count] = item; + collection->view_count++; + + gal_view_collection_changed (collection); + return item->id; +} + +const gchar * +gal_view_collection_set_nth_view (GalViewCollection *collection, + gint i, + GalView *view) +{ + GalViewCollectionItem *item; + + g_return_val_if_fail (GAL_IS_VIEW_COLLECTION (collection), NULL); + g_return_val_if_fail (GAL_IS_VIEW (view), NULL); + g_return_val_if_fail (i >= 0, NULL); + g_return_val_if_fail (i < collection->view_count, NULL); + + d (g_print ("%s: %p\n", G_STRFUNC, view)); + + item = collection->view_data[i]; + + gal_view_set_title (view, item->title); + g_object_ref (view); + if (item->view) { + g_signal_handler_disconnect ( + item->view, + item->view_changed_id); + g_object_unref (item->view); + } + item->view = view; + + item->ever_changed = TRUE; + item->changed = TRUE; + item->type = g_strdup (gal_view_get_type_code (view)); + + item->view_changed_id = g_signal_connect ( + item->view, "changed", + G_CALLBACK (view_changed), item); + + gal_view_collection_changed (collection); + return item->id; +} + +const gchar * +gal_view_collection_get_default_view (GalViewCollection *collection) +{ + return collection->default_view; +} + +void +gal_view_collection_set_default_view (GalViewCollection *collection, + const gchar *id) +{ + g_free (collection->default_view); + collection->default_view = g_strdup (id); + gal_view_collection_changed (collection); + collection->default_view_built_in = FALSE; +} + diff --git a/e-util/gal-view-collection.h b/e-util/gal-view-collection.h new file mode 100644 index 0000000000..980f7c0365 --- /dev/null +++ b/e-util/gal-view-collection.h @@ -0,0 +1,150 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _GAL_VIEW_SET_H_ +#define _GAL_VIEW_SET_H_ + +#include <e-util/gal-view-factory.h> + +G_BEGIN_DECLS + +#define GAL_VIEW_COLLECTION_TYPE (gal_view_collection_get_type ()) +#define GAL_VIEW_COLLECTION(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GAL_VIEW_COLLECTION_TYPE, GalViewCollection)) +#define GAL_VIEW_COLLECTION_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GAL_VIEW_COLLECTION_TYPE, GalViewCollectionClass)) +#define GAL_IS_VIEW_COLLECTION(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GAL_VIEW_COLLECTION_TYPE)) +#define GAL_IS_VIEW_COLLECTION_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GAL_VIEW_COLLECTION_TYPE)) +#define GAL_VIEW_COLLECTION_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GAL_VIEW_COLLECTION_TYPE, GalViewCollectionClass)) + +typedef struct GalViewCollectionItem GalViewCollectionItem; + +typedef struct { + GObject base; + + GalViewCollectionItem **view_data; + gint view_count; + + GList *factory_list; + + GalViewCollectionItem **removed_view_data; + gint removed_view_count; + + guint loaded : 1; + guint default_view_built_in : 1; + + gchar *system_dir; + gchar *local_dir; + + gchar *default_view; + + gchar *title; +} GalViewCollection; + +typedef struct { + GObjectClass parent_class; + + /* + * Signals + */ + void (*display_view) (GalViewCollection *collection, + GalView *view); + void (*changed) (GalViewCollection *collection); +} GalViewCollectionClass; + +struct GalViewCollectionItem { + GalView *view; + gchar *id; + guint changed : 1; + guint ever_changed : 1; + guint built_in : 1; + gchar *filename; + gchar *title; + gchar *type; + GalViewCollection *collection; + guint view_changed_id; +}; + +/* Standard functions */ +GType gal_view_collection_get_type (void); +GalViewCollection *gal_view_collection_new (void); + +void gal_view_collection_set_title (GalViewCollection *collection, + const gchar *title); +/* Set up the view collection. Call these two functions before ever doing load or save and never call them again. */ +void gal_view_collection_set_storage_directories (GalViewCollection *collection, + const gchar *system_dir, + const gchar *local_dir); +void gal_view_collection_add_factory (GalViewCollection *collection, + GalViewFactory *factory); + +/* Send the display view signal. This function is deprecated. */ +void gal_view_collection_display_view (GalViewCollection *collection, + GalView *view); + +/* Query the view collection. */ +gint gal_view_collection_get_count (GalViewCollection *collection); +GalView *gal_view_collection_get_view (GalViewCollection *collection, + gint n); +GalViewCollectionItem *gal_view_collection_get_view_item (GalViewCollection *collection, + gint n); +gint gal_view_collection_get_view_index_by_id (GalViewCollection *collection, + const gchar *view_id); +gchar *gal_view_collection_get_view_id_by_index (GalViewCollection *collection, + gint n); + +/* Manipulate the view collection */ +void gal_view_collection_append (GalViewCollection *collection, + GalView *view); +void gal_view_collection_delete_view (GalViewCollection *collection, + gint i); +void gal_view_collection_copy_view (GalViewCollection *collection, + gint i); +/* Call set_storage_directories and add factories for anything that + * might be found there before doing either of these. */ +void gal_view_collection_load (GalViewCollection *collection); +void gal_view_collection_save (GalViewCollection *collection); +gboolean gal_view_collection_loaded (GalViewCollection *collection); + +/* Use factory list to load a GalView file. */ +GalView *gal_view_collection_load_view_from_file (GalViewCollection *collection, + const gchar *type, + const gchar *filename); + +/* Returns id of the new view. These functions are used for + * GalViewInstanceSaveAsDialog. */ +const gchar *gal_view_collection_append_with_title (GalViewCollection *collection, + const gchar *title, + GalView *view); +const gchar *gal_view_collection_set_nth_view (GalViewCollection *collection, + gint i, + GalView *view); + +const gchar *gal_view_collection_get_default_view (GalViewCollection *collection); +void gal_view_collection_set_default_view (GalViewCollection *collection, + const gchar *id); + +G_END_DECLS + +#endif /* _GAL_VIEW_COLLECTION_H_ */ diff --git a/e-util/gal-view-etable.c b/e-util/gal-view-etable.c new file mode 100644 index 0000000000..3f50e2881a --- /dev/null +++ b/e-util/gal-view-etable.c @@ -0,0 +1,335 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-view-etable.h" + +#include "e-table-config.h" + +G_DEFINE_TYPE (GalViewEtable, gal_view_etable, GAL_TYPE_VIEW) + +static void +detach_table (GalViewEtable *view) +{ + if (view->table == NULL) + return; + if (view->table_state_changed_id) { + g_signal_handler_disconnect ( + view->table, + view->table_state_changed_id); + view->table_state_changed_id = 0; + } + g_object_unref (view->table); + view->table = NULL; +} + +static void +detach_tree (GalViewEtable *view) +{ + if (view->tree == NULL) + return; + if (view->tree_state_changed_id) { + g_signal_handler_disconnect ( + view->tree, + view->tree_state_changed_id); + view->tree_state_changed_id = 0; + } + g_object_unref (view->tree); + view->tree = NULL; +} + +static void +config_changed (ETableConfig *config, + GalViewEtable *view) +{ + ETableState *state; + if (view->state) + g_object_unref (view->state); + g_object_get ( + config, + "state", &state, + NULL); + view->state = e_table_state_duplicate (state); + g_object_unref (state); + + gal_view_changed (GAL_VIEW (view)); +} + +static void +gal_view_etable_edit (GalView *view, + GtkWindow *parent) +{ + GalViewEtable *etable_view = GAL_VIEW_ETABLE (view); + ETableConfig *config; + + config = e_table_config_new ( + etable_view->title, + etable_view->spec, + etable_view->state, + parent); + + g_signal_connect ( + config, "changed", + G_CALLBACK (config_changed), view); +} + +static void +gal_view_etable_load (GalView *view, + const gchar *filename) +{ + e_table_state_load_from_file (GAL_VIEW_ETABLE (view)->state, filename); +} + +static void +gal_view_etable_save (GalView *view, + const gchar *filename) +{ + e_table_state_save_to_file (GAL_VIEW_ETABLE (view)->state, filename); +} + +static const gchar * +gal_view_etable_get_title (GalView *view) +{ + return GAL_VIEW_ETABLE (view)->title; +} + +static void +gal_view_etable_set_title (GalView *view, + const gchar *title) +{ + g_free (GAL_VIEW_ETABLE (view)->title); + GAL_VIEW_ETABLE (view)->title = g_strdup (title); +} + +static const gchar * +gal_view_etable_get_type_code (GalView *view) +{ + return "etable"; +} + +static GalView * +gal_view_etable_clone (GalView *view) +{ + GalViewEtable *gve, *new; + + gve = GAL_VIEW_ETABLE (view); + + new = g_object_new (GAL_TYPE_VIEW_ETABLE, NULL); + new->spec = gve->spec; + new->title = g_strdup (gve->title); + g_object_unref (new->state); + new->state = e_table_state_duplicate (gve->state); + + g_object_ref (new->spec); + + return GAL_VIEW (new); +} + +static void +gal_view_etable_dispose (GObject *object) +{ + GalViewEtable *view = GAL_VIEW_ETABLE (object); + + gal_view_etable_detach (view); + + g_free (view->title); + view->title = NULL; + + if (view->spec) + g_object_unref (view->spec); + view->spec = NULL; + + if (view->state) + g_object_unref (view->state); + view->state = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (gal_view_etable_parent_class)->dispose (object); +} + +static void +gal_view_etable_class_init (GalViewEtableClass *class) +{ + GalViewClass *gal_view_class = GAL_VIEW_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + gal_view_class->edit = gal_view_etable_edit; + gal_view_class->load = gal_view_etable_load; + gal_view_class->save = gal_view_etable_save; + gal_view_class->get_title = gal_view_etable_get_title; + gal_view_class->set_title = gal_view_etable_set_title; + gal_view_class->get_type_code = gal_view_etable_get_type_code; + gal_view_class->clone = gal_view_etable_clone; + + object_class->dispose = gal_view_etable_dispose; +} + +static void +gal_view_etable_init (GalViewEtable *gve) +{ + gve->spec = NULL; + gve->state = e_table_state_new (); + gve->title = NULL; +} + +/** + * gal_view_etable_new + * @spec: The ETableSpecification that this view will be based upon. + * @title: The name of the new view. + * + * Returns a new GalViewEtable. This is primarily for use by + * GalViewFactoryEtable. + * + * Returns: The new GalViewEtable. + */ +GalView * +gal_view_etable_new (ETableSpecification *spec, + const gchar *title) +{ + GalViewEtable *view; + + g_return_val_if_fail (E_IS_TABLE_SPECIFICATION (spec), NULL); + + view = g_object_new (GAL_TYPE_VIEW_ETABLE, NULL); + + return gal_view_etable_construct (view, spec, title); +} + +/** + * gal_view_etable_construct + * @view: The view to construct. + * @spec: The ETableSpecification that this view will be based upon. + * @title: The name of the new view. + * + * constructs the GalViewEtable. To be used by subclasses and + * language bindings. + * + * Returns: The GalViewEtable. + */ +GalView * +gal_view_etable_construct (GalViewEtable *view, + ETableSpecification *spec, + const gchar *title) +{ + g_return_val_if_fail (GAL_IS_VIEW_ETABLE (view), NULL); + g_return_val_if_fail (E_IS_TABLE_SPECIFICATION (spec), NULL); + + view->spec = g_object_ref (spec); + + if (view->state) + g_object_unref (view->state); + view->state = e_table_state_duplicate (spec->state); + + view->title = g_strdup (title); + + return GAL_VIEW (view); +} + +void +gal_view_etable_set_state (GalViewEtable *view, + ETableState *state) +{ + g_return_if_fail (GAL_IS_VIEW_ETABLE (view)); + g_return_if_fail (E_IS_TABLE_STATE (state)); + + if (view->state) + g_object_unref (view->state); + view->state = e_table_state_duplicate (state); + + gal_view_changed (GAL_VIEW (view)); +} + +static void +table_state_changed (ETable *table, + GalViewEtable *view) +{ + ETableState *state; + + state = e_table_get_state_object (table); + g_object_unref (view->state); + view->state = state; + + gal_view_changed (GAL_VIEW (view)); +} + +static void +tree_state_changed (ETree *tree, + GalViewEtable *view) +{ + ETableState *state; + + state = e_tree_get_state_object (tree); + g_object_unref (view->state); + view->state = state; + + gal_view_changed (GAL_VIEW (view)); +} + +void +gal_view_etable_attach_table (GalViewEtable *view, + ETable *table) +{ + g_return_if_fail (GAL_IS_VIEW_ETABLE (view)); + g_return_if_fail (E_IS_TABLE (table)); + + gal_view_etable_detach (view); + + view->table = table; + + e_table_set_state_object (view->table, view->state); + g_object_ref (view->table); + view->table_state_changed_id = g_signal_connect ( + view->table, "state_change", + G_CALLBACK (table_state_changed), view); +} + +void +gal_view_etable_attach_tree (GalViewEtable *view, + ETree *tree) +{ + g_return_if_fail (GAL_IS_VIEW_ETABLE (view)); + g_return_if_fail (E_IS_TREE (tree)); + + gal_view_etable_detach (view); + + view->tree = tree; + + e_tree_set_state_object (view->tree, view->state); + g_object_ref (view->tree); + view->tree_state_changed_id = g_signal_connect ( + view->tree, "state_change", + G_CALLBACK (tree_state_changed), view); +} + +void +gal_view_etable_detach (GalViewEtable *view) +{ + g_return_if_fail (GAL_IS_VIEW_ETABLE (view)); + + if (view->table != NULL) + detach_table (view); + if (view->tree != NULL) + detach_tree (view); +} diff --git a/e-util/gal-view-etable.h b/e-util/gal-view-etable.h new file mode 100644 index 0000000000..92f7e64efb --- /dev/null +++ b/e-util/gal-view-etable.h @@ -0,0 +1,96 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef GAL_VIEW_ETABLE_H +#define GAL_VIEW_ETABLE_H + +#include <gtk/gtk.h> +#include <e-util/gal-view.h> +#include <e-util/e-table-state.h> +#include <e-util/e-table-specification.h> +#include <e-util/e-table.h> +#include <e-util/e-tree.h> + +/* Standard GObject macros */ +#define GAL_TYPE_VIEW_ETABLE \ + (gal_view_etable_get_type ()) +#define GAL_VIEW_ETABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), GAL_TYPE_VIEW_ETABLE, GalViewEtable)) +#define GAL_VIEW_ETABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), GAL_TYPE_VIEW_ETABLE, GalViewEtableClass)) +#define GAL_IS_VIEW_ETABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), GAL_TYPE_VIEW_ETABLE)) +#define GAL_IS_VIEW_ETABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), GAL_TYPE_VIEW_ETABLE)) +#define GAL_VIEW_ETABLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), GAL_TYPE_VIEW_ETABLE, GalViewEtableClass)) + +G_BEGIN_DECLS + +typedef struct _GalViewEtable GalViewEtable; +typedef struct _GalViewEtableClass GalViewEtableClass; + +struct _GalViewEtable { + GalView parent; + + ETableSpecification *spec; + ETableState *state; + gchar *title; + + ETable *table; + guint table_state_changed_id; + + ETree *tree; + guint tree_state_changed_id; +}; + +struct _GalViewEtableClass { + GalViewClass parent_class; +}; + +GType gal_view_etable_get_type (void); +GalView * gal_view_etable_new (ETableSpecification *spec, + const gchar *title); +GalView * gal_view_etable_construct (GalViewEtable *view, + ETableSpecification *spec, + const gchar *title); +void gal_view_etable_set_state (GalViewEtable *view, + ETableState *state); +void gal_view_etable_attach_table (GalViewEtable *view, + ETable *table); +void gal_view_etable_attach_tree (GalViewEtable *view, + ETree *tree); +void gal_view_etable_detach (GalViewEtable *view); + +G_END_DECLS + +#endif /* GAL_VIEW_ETABLE_H */ diff --git a/e-util/gal-view-factory-etable.c b/e-util/gal-view-factory-etable.c new file mode 100644 index 0000000000..632c959a85 --- /dev/null +++ b/e-util/gal-view-factory-etable.c @@ -0,0 +1,195 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <glib/gi18n.h> + +#include "gal-view-etable.h" +#include "gal-view-factory-etable.h" + +#define GAL_VIEW_FACTORY_ETABLE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), GAL_TYPE_VIEW_FACTORY_ETABLE, GalViewFactoryEtablePrivate)) + +struct _GalViewFactoryEtablePrivate { + ETableSpecification *specification; +}; + +enum { + PROP_0, + PROP_SPECIFICATION +}; + +G_DEFINE_TYPE ( + GalViewFactoryEtable, + gal_view_factory_etable, GAL_TYPE_VIEW_FACTORY) + +static void +view_factory_etable_set_specification (GalViewFactoryEtable *factory, + ETableSpecification *specification) +{ + g_return_if_fail (factory->priv->specification == NULL); + g_return_if_fail (E_IS_TABLE_SPECIFICATION (specification)); + + factory->priv->specification = g_object_ref (specification); +} + +static void +view_factory_etable_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SPECIFICATION: + view_factory_etable_set_specification ( + GAL_VIEW_FACTORY_ETABLE (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +view_factory_etable_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SPECIFICATION: + g_value_set_object ( + value, + gal_view_factory_etable_get_specification ( + GAL_VIEW_FACTORY_ETABLE (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +view_factory_etable_dispose (GObject *object) +{ + GalViewFactoryEtablePrivate *priv; + + priv = GAL_VIEW_FACTORY_ETABLE_GET_PRIVATE (object); + + if (priv->specification != NULL) { + g_object_unref (priv->specification); + priv->specification = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (gal_view_factory_etable_parent_class)->dispose (object); +} + +static const gchar * +view_factory_etable_get_title (GalViewFactory *factory) +{ + return _("Table"); +} + +static const gchar * +view_factory_etable_get_type_code (GalViewFactory *factory) +{ + return "etable"; +} + +static GalView * +view_factory_etable_new_view (GalViewFactory *factory, + const gchar *name) +{ + GalViewFactoryEtablePrivate *priv; + + priv = GAL_VIEW_FACTORY_ETABLE_GET_PRIVATE (factory); + + return gal_view_etable_new (priv->specification, name); +} + +static void +gal_view_factory_etable_class_init (GalViewFactoryEtableClass *class) +{ + GObjectClass *object_class; + GalViewFactoryClass *view_factory_class; + + g_type_class_add_private (class, sizeof (GalViewFactoryEtablePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = view_factory_etable_set_property; + object_class->get_property = view_factory_etable_get_property; + object_class->dispose = view_factory_etable_dispose; + + view_factory_class = GAL_VIEW_FACTORY_CLASS (class); + view_factory_class->get_title = view_factory_etable_get_title; + view_factory_class->get_type_code = view_factory_etable_get_type_code; + view_factory_class->new_view = view_factory_etable_new_view; + + g_object_class_install_property ( + object_class, + PROP_SPECIFICATION, + g_param_spec_object ( + "specification", + NULL, + NULL, + E_TYPE_TABLE_SPECIFICATION, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); +} + +static void +gal_view_factory_etable_init (GalViewFactoryEtable *factory) +{ + factory->priv = GAL_VIEW_FACTORY_ETABLE_GET_PRIVATE (factory); +} + +/** + * gal_view_etable_new: + * @specification: The spec to create GalViewEtables based upon. + * + * A new GalViewFactory for creating ETable views. Create one of + * these and pass it to GalViewCollection for use. + * + * Returns: The new GalViewFactoryEtable. + */ +GalViewFactory * +gal_view_factory_etable_new (ETableSpecification *specification) +{ + g_return_val_if_fail (E_IS_TABLE_SPECIFICATION (specification), NULL); + + return g_object_new ( + GAL_TYPE_VIEW_FACTORY_ETABLE, + "specification", specification, NULL); +} + +ETableSpecification * +gal_view_factory_etable_get_specification (GalViewFactoryEtable *factory) +{ + g_return_val_if_fail (GAL_IS_VIEW_FACTORY_ETABLE (factory), NULL); + + return factory->priv->specification; +} diff --git a/e-util/gal-view-factory-etable.h b/e-util/gal-view-factory-etable.h new file mode 100644 index 0000000000..cc4b617448 --- /dev/null +++ b/e-util/gal-view-factory-etable.h @@ -0,0 +1,77 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef GAL_VIEW_FACTORY_ETABLE_H +#define GAL_VIEW_FACTORY_ETABLE_H + +#include <gtk/gtk.h> +#include <e-util/gal-view-factory.h> +#include <e-util/e-table-specification.h> + +/* Standard GObject macros */ +#define GAL_TYPE_VIEW_FACTORY_ETABLE \ + (gal_view_factory_etable_get_type ()) +#define GAL_VIEW_FACTORY_ETABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), GAL_TYPE_VIEW_FACTORY_ETABLE, GalViewFactoryEtable)) +#define GAL_VIEW_FACTORY_ETABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), GAL_TYPE_VIEW_FACTORY_ETABLE, GalViewFactoryEtableClass)) +#define GAL_IS_VIEW_FACTORY_ETABLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), GAL_TYPE_VIEW_FACTORY_ETABLE)) +#define GAL_IS_VIEW_FACTORY_ETABLE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), GAL_TYPE_VIEW_FACTORY_ETABLE)) +#define GAL_VIEW_FACTORY_ETABLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), GAL_TYPE_VIEW_FACTORY_ETABLE, GalViewFactoryEtableClass)) + +G_BEGIN_DECLS + +typedef struct _GalViewFactoryEtable GalViewFactoryEtable; +typedef struct _GalViewFactoryEtableClass GalViewFactoryEtableClass; +typedef struct _GalViewFactoryEtablePrivate GalViewFactoryEtablePrivate; + +struct _GalViewFactoryEtable { + GalViewFactory parent; + GalViewFactoryEtablePrivate *priv; +}; + +struct _GalViewFactoryEtableClass { + GalViewFactoryClass parent_class; +}; + +GType gal_view_factory_etable_get_type (void); +ETableSpecification * + gal_view_factory_etable_get_specification + (GalViewFactoryEtable *factory); +GalViewFactory *gal_view_factory_etable_new (ETableSpecification *specification); + +G_END_DECLS + +#endif /* GAL_VIEW_FACTORY_ETABLE_H */ diff --git a/e-util/gal-view-factory.c b/e-util/gal-view-factory.c new file mode 100644 index 0000000000..0e0dde05cb --- /dev/null +++ b/e-util/gal-view-factory.c @@ -0,0 +1,101 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-view-factory.h" + +G_DEFINE_TYPE (GalViewFactory, gal_view_factory, G_TYPE_OBJECT) + +/* XXX Should GalViewFactory be a GInterface? */ + +static void +gal_view_factory_class_init (GalViewFactoryClass *class) +{ +} + +static void +gal_view_factory_init (GalViewFactory *factory) +{ +} + +/** + * gal_view_factory_get_title: + * @factory: a #GalViewFactory + * + * Returns: The title of the factory. + */ +const gchar * +gal_view_factory_get_title (GalViewFactory *factory) +{ + GalViewFactoryClass *class; + + g_return_val_if_fail (GAL_IS_VIEW_FACTORY (factory), NULL); + + class = GAL_VIEW_FACTORY_GET_CLASS (factory); + g_return_val_if_fail (class->get_title != NULL, NULL); + + return class->get_title (factory); +} + +/** + * gal_view_factory_get_type_code: + * @factory: a #GalViewFactory + * + * Returns: The type code + */ +const gchar * +gal_view_factory_get_type_code (GalViewFactory *factory) +{ + GalViewFactoryClass *class; + + g_return_val_if_fail (GAL_IS_VIEW_FACTORY (factory), NULL); + + class = GAL_VIEW_FACTORY_GET_CLASS (factory); + g_return_val_if_fail (class->get_type_code != NULL, NULL); + + return class->get_type_code (factory); +} + +/** + * gal_view_factory_new_view: + * @factory: a #GalViewFactory + * @name: the name for the view + * + * Returns: The new view + */ +GalView * +gal_view_factory_new_view (GalViewFactory *factory, + const gchar *name) +{ + GalViewFactoryClass *class; + + g_return_val_if_fail (GAL_IS_VIEW_FACTORY (factory), NULL); + + class = GAL_VIEW_FACTORY_GET_CLASS (factory); + g_return_val_if_fail (class->new_view != NULL, NULL); + + return class->new_view (factory, name); +} + diff --git a/e-util/gal-view-factory.h b/e-util/gal-view-factory.h new file mode 100644 index 0000000000..abdcacd6ac --- /dev/null +++ b/e-util/gal-view-factory.h @@ -0,0 +1,85 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef GAL_VIEW_FACTORY_H +#define GAL_VIEW_FACTORY_H + +#include <e-util/gal-view.h> + +/* Standard GObject macros */ +#define GAL_TYPE_VIEW_FACTORY \ + (gal_view_factory_get_type ()) +#define GAL_VIEW_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), GAL_TYPE_VIEW_FACTORY, GalViewFactory)) +#define GAL_VIEW_FACTORY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), GAL_TYPE_VIEW_FACTORY, GalViewFactoryClass)) +#define GAL_IS_VIEW_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), GAL_TYPE_VIEW_FACTORY)) +#define GAL_IS_VIEW_FACTORY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), GAL_TYPE_VIEW_FACTORY)) +#define GAL_VIEW_FACTORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), GAL_TYPE_VIEW_FACTORY, GalViewFactoryClass)) + +G_BEGIN_DECLS + +typedef struct _GalViewFactory GalViewFactory; +typedef struct _GalViewFactoryClass GalViewFactoryClass; + +struct _GalViewFactory { + GObject parent; +}; + +struct _GalViewFactoryClass { + GObjectClass parent_class; + + /* Methods */ + const gchar * (*get_title) (GalViewFactory *factory); + const gchar * (*get_type_code) (GalViewFactory *factory); + GalView * (*new_view) (GalViewFactory *factory, + const gchar *name); +}; + +GType gal_view_factory_get_type (void); +const gchar * gal_view_factory_get_title (GalViewFactory *factory); + +/* Returns the code for use in identifying this type of object in the + * view list. This identifier should identify this as being the + * unique factory for xml files which were written out with this + * identifier. Thus each factory should have a unique type code. */ +const gchar * gal_view_factory_get_type_code (GalViewFactory *factory); + +GalView * gal_view_factory_new_view (GalViewFactory *factory, + const gchar *name); + +G_END_DECLS + +#endif /* GAL_VIEW_FACTORY_H */ diff --git a/e-util/gal-view-instance-save-as-dialog.c b/e-util/gal-view-instance-save-as-dialog.c new file mode 100644 index 0000000000..c71892e4ff --- /dev/null +++ b/e-util/gal-view-instance-save-as-dialog.c @@ -0,0 +1,359 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-view-instance-save-as-dialog.h" + +#include <glib/gi18n.h> + +#include "e-misc-utils.h" +#include "e-util-private.h" +#include "gal-define-views-model.h" +#include "gal-view-new-dialog.h" + +G_DEFINE_TYPE (GalViewInstanceSaveAsDialog, gal_view_instance_save_as_dialog, GTK_TYPE_DIALOG) + +enum { + PROP_0, + PROP_INSTANCE +}; + +enum { + COL_GALVIEW_NAME, + COL_GALVIEW_DATA +}; + +/* Static functions */ +static void +gal_view_instance_save_as_dialog_set_instance (GalViewInstanceSaveAsDialog *dialog, + GalViewInstance *instance) +{ + gint i; + GtkListStore *store; + GtkCellRenderer *renderer; + dialog->instance = instance; + + store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_POINTER); + + for (i = 0; i < instance->collection->view_count; i++) { + GalViewCollectionItem *item = instance->collection->view_data[i]; + GtkTreeIter iter; + gchar *title = NULL; + + /* hide built in views */ + /*if (item->built_in == 1) + continue;*/ + + title = e_str_without_underscores (item->title); + + gtk_list_store_append (store, &iter); + gtk_list_store_set ( + store, &iter, + COL_GALVIEW_NAME, title, + COL_GALVIEW_DATA, item, + -1); + + g_free (title); + } + + gtk_tree_sortable_set_sort_column_id ( + GTK_TREE_SORTABLE (store), + COL_GALVIEW_NAME, GTK_SORT_ASCENDING); + + /* attaching treeview to model */ + gtk_tree_view_set_model (dialog->treeview, GTK_TREE_MODEL (store)); + gtk_tree_view_set_search_column (dialog->treeview, COL_GALVIEW_NAME); + + dialog->model = GTK_TREE_MODEL (store); + + renderer = gtk_cell_renderer_text_new (); + + gtk_tree_view_insert_column_with_attributes ( + dialog->treeview, + COL_GALVIEW_NAME, _("Name"), + renderer, "text", COL_GALVIEW_NAME, + NULL); + + /* set sort column */ + gtk_tree_sortable_set_sort_column_id ( + GTK_TREE_SORTABLE (dialog->model), + COL_GALVIEW_NAME, GTK_SORT_ASCENDING); +} + +static void +gvisad_setup_validate_button (GalViewInstanceSaveAsDialog *dialog) +{ + if ((dialog->toggle == GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_TOGGLE_CREATE + && g_utf8_strlen (gtk_entry_get_text (GTK_ENTRY (dialog->entry_create)), -1) > 0) + || dialog->toggle == GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_TOGGLE_REPLACE) { + gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog), GTK_RESPONSE_OK, TRUE); + } else { + gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog), GTK_RESPONSE_OK, FALSE); + } +} + +static void +gvisad_setup_radio_buttons (GalViewInstanceSaveAsDialog *dialog) +{ + GtkWidget *widget; + + widget = dialog->scrolledwindow; + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->radiobutton_replace))) { + GtkTreeIter iter; + GtkTreeSelection *selection; + + selection = gtk_tree_view_get_selection (dialog->treeview); + if (!gtk_tree_selection_get_selected (selection, &dialog->model, &iter)) { + if (gtk_tree_model_get_iter_first (dialog->model, &iter)) { + gtk_tree_selection_select_iter (selection, &iter); + } + } + + gtk_widget_set_sensitive (widget, TRUE); + dialog->toggle = GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_TOGGLE_REPLACE; + } else { + gtk_widget_set_sensitive (widget, FALSE); + } + + widget = dialog->entry_create; + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->radiobutton_create))) { + gtk_widget_set_sensitive (widget, TRUE); + dialog->toggle = GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_TOGGLE_CREATE; + } else { + gtk_widget_set_sensitive (widget, FALSE); + } + + gvisad_setup_validate_button (dialog); +} + +static void +gvisad_radio_toggled (GtkWidget *widget, + GalViewInstanceSaveAsDialog *dialog) +{ + gvisad_setup_radio_buttons (dialog); +} + +static void +gvisad_entry_changed (GtkWidget *widget, + GalViewInstanceSaveAsDialog *dialog) +{ + gvisad_setup_validate_button (dialog); +} + +/* Method override implementations */ +static void +gal_view_instance_save_as_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GalViewInstanceSaveAsDialog *dialog; + + dialog = GAL_VIEW_INSTANCE_SAVE_AS_DIALOG (object); + + switch (property_id) { + case PROP_INSTANCE: + if (g_value_get_object (value)) + gal_view_instance_save_as_dialog_set_instance (dialog, GAL_VIEW_INSTANCE (g_value_get_object (value))); + else + gal_view_instance_save_as_dialog_set_instance (dialog, NULL); + break; + + default: + return; + } +} + +static void +gal_view_instance_save_as_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GalViewInstanceSaveAsDialog *dialog; + + dialog = GAL_VIEW_INSTANCE_SAVE_AS_DIALOG (object); + + switch (property_id) { + case PROP_INSTANCE: + g_value_set_object (value, dialog->instance); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gal_view_instance_save_as_dialog_dispose (GObject *object) +{ + GalViewInstanceSaveAsDialog *gal_view_instance_save_as_dialog = GAL_VIEW_INSTANCE_SAVE_AS_DIALOG (object); + + if (gal_view_instance_save_as_dialog->builder) + g_object_unref (gal_view_instance_save_as_dialog->builder); + gal_view_instance_save_as_dialog->builder = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (gal_view_instance_save_as_dialog_parent_class)->dispose (object); +} + +/* Init functions */ +static void +gal_view_instance_save_as_dialog_class_init (GalViewInstanceSaveAsDialogClass *class) +{ + GObjectClass *object_class; + + object_class = (GObjectClass *) class; + + object_class->set_property = gal_view_instance_save_as_dialog_set_property; + object_class->get_property = gal_view_instance_save_as_dialog_get_property; + object_class->dispose = gal_view_instance_save_as_dialog_dispose; + + g_object_class_install_property ( + object_class, + PROP_INSTANCE, + g_param_spec_object ( + "instance", + "Instance", + NULL, + GAL_VIEW_INSTANCE_TYPE, + G_PARAM_READWRITE)); +} + +static void +gal_view_instance_save_as_dialog_init (GalViewInstanceSaveAsDialog *dialog) +{ + GtkWidget *content_area; + GtkWidget *widget; + + dialog->instance = NULL; + dialog->model = NULL; + dialog->collection = NULL; + + dialog->builder = gtk_builder_new (); + e_load_ui_builder_definition ( + dialog->builder, "gal-view-instance-save-as-dialog.ui"); + + widget = e_builder_get_widget (dialog->builder, "vbox-top"); + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_box_pack_start (GTK_BOX (content_area), widget, TRUE, TRUE, 0); + + /* TODO: add position/size saving/restoring */ + gtk_container_set_border_width (GTK_CONTAINER (dialog), 5); + gtk_window_set_default_size (GTK_WINDOW (dialog), 300, 360); + + gtk_dialog_add_buttons ( + GTK_DIALOG (dialog), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_SAVE, GTK_RESPONSE_OK, + NULL); + + dialog->scrolledwindow = e_builder_get_widget (dialog->builder, "scrolledwindow2"); + dialog->treeview = GTK_TREE_VIEW (e_builder_get_widget (dialog->builder, "custom-replace")); + dialog->entry_create = e_builder_get_widget (dialog->builder, "entry-create"); + dialog->radiobutton_replace = e_builder_get_widget (dialog->builder, "radiobutton-replace"); + dialog->radiobutton_create = e_builder_get_widget (dialog->builder, "radiobutton-create"); + + gtk_tree_view_set_reorderable (GTK_TREE_VIEW (dialog->treeview), FALSE); + gtk_tree_view_set_headers_visible (dialog->treeview, FALSE); + + g_signal_connect ( + dialog->radiobutton_replace, "toggled", + G_CALLBACK (gvisad_radio_toggled), dialog); + g_signal_connect ( + dialog->radiobutton_create, "toggled", + G_CALLBACK (gvisad_radio_toggled), dialog); + g_signal_connect ( + dialog->entry_create, "changed", + G_CALLBACK (gvisad_entry_changed), dialog); + + gvisad_setup_radio_buttons (dialog); + gvisad_setup_validate_button (dialog); + + gtk_window_set_title (GTK_WINDOW (dialog), _("Save Current View")); + gtk_widget_show (GTK_WIDGET (dialog)); +} + +/* External methods */ +/** + * gal_view_instance_save_as_dialog_new + * + * Returns a new dialog for defining views. + * + * Returns: The GalViewInstanceSaveAsDialog. + */ +GtkWidget * +gal_view_instance_save_as_dialog_new (GalViewInstance *instance) +{ + GtkWidget *widget = g_object_new (GAL_TYPE_VIEW_INSTANCE_SAVE_AS_DIALOG, NULL); + gal_view_instance_save_as_dialog_set_instance (GAL_VIEW_INSTANCE_SAVE_AS_DIALOG (widget), instance); + return widget; +} + +void +gal_view_instance_save_as_dialog_save (GalViewInstanceSaveAsDialog *dialog) +{ + GalView *view = gal_view_instance_get_current_view (dialog->instance); + const gchar *title; + gint n; + const gchar *id = NULL; + GalViewCollectionItem *item; + + view = gal_view_clone (view); + switch (dialog->toggle) { + case GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_TOGGLE_REPLACE: + if (dialog->treeview) { + GtkTreeIter iter; + GtkTreeSelection *selection; + + selection = gtk_tree_view_get_selection (dialog->treeview); + if (gtk_tree_selection_get_selected (selection, &dialog->model, &iter)) { + gtk_tree_model_get (dialog->model, &iter, COL_GALVIEW_DATA, &item, -1); + + for (n = 0; n < dialog->instance->collection->view_count; n++) { + if (item == dialog->instance->collection->view_data[n]) { + id = gal_view_collection_set_nth_view (dialog->instance->collection, n, view); + gal_view_collection_save (dialog->instance->collection); + } + } + } + + } + break; + + case GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_TOGGLE_CREATE: + if (dialog->entry_create && GTK_IS_ENTRY (dialog->entry_create)) { + title = gtk_entry_get_text (GTK_ENTRY (dialog->entry_create)); + id = gal_view_collection_append_with_title (dialog->instance->collection, title, view); + gal_view_collection_save (dialog->instance->collection); + } + break; + } + + if (id) { + gal_view_instance_set_current_view_id (dialog->instance, id); + } +} diff --git a/e-util/gal-view-instance-save-as-dialog.h b/e-util/gal-view-instance-save-as-dialog.h new file mode 100644 index 0000000000..47b76b1155 --- /dev/null +++ b/e-util/gal-view-instance-save-as-dialog.h @@ -0,0 +1,92 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_H +#define GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_H + +#include <gtk/gtk.h> +#include <e-util/gal-view-collection.h> +#include <e-util/gal-view-instance.h> + +/* Standard GObject macros */ +#define GAL_TYPE_VIEW_INSTANCE_SAVE_AS_DIALOG \ + (gal_view_instance_save_as_dialog_get_type ()) +#define GAL_VIEW_INSTANCE_SAVE_AS_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), GAL_TYPE_VIEW_INSTANCE_SAVE_AS_DIALOG, GalViewInstanceSaveAsDialog)) +#define GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), GAL_TYPE_VIEW_INSTANCE_SAVE_AS_DIALOG, GalViewInstanceSaveAsDialogClass)) +#define GAL_IS_VIEW_INSTANCE_SAVE_AS_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), GAL_TYPE_VIEW_INSTANCE_SAVE_AS_DIALOG)) +#define GAL_IS_VIEW_INSTANCE_SAVE_AS_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), GAL_TYPE_VIEW_INSTANCE_SAVE_AS_DIALOG)) +#define GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), GAL_TYPE_VIEW_INSTANCE_SAVE_AS_DIALOG, GalViewInstanceSaveAsDialogClass)) + +G_BEGIN_DECLS + +typedef struct _GalViewInstanceSaveAsDialog GalViewInstanceSaveAsDialog; +typedef struct _GalViewInstanceSaveAsDialogClass GalViewInstanceSaveAsDialogClass; + +typedef enum { + GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_TOGGLE_REPLACE, + GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_TOGGLE_CREATE +} GalViewInstanceSaveAsDialogToggle; + +struct _GalViewInstanceSaveAsDialog { + GtkDialog parent; + + /* item specific fields */ + GtkBuilder *builder; + GtkTreeView *treeview; + GtkTreeModel *model; + + GtkWidget *scrolledwindow, *radiobutton_replace; + GtkWidget *entry_create, *radiobutton_create; + + GalViewInstance *instance; + GalViewCollection *collection; + + GalViewInstanceSaveAsDialogToggle toggle; +}; + +struct _GalViewInstanceSaveAsDialogClass { + GtkDialogClass parent_class; +}; + +GType gal_view_instance_save_as_dialog_get_type (void); +GtkWidget * gal_view_instance_save_as_dialog_new + (GalViewInstance *instance); +void gal_view_instance_save_as_dialog_save + (GalViewInstanceSaveAsDialog *dialog); + +G_END_DECLS + +#endif /* GAL_VIEW_INSTANCE_SAVE_AS_DIALOG_H */ diff --git a/e-util/gal-view-instance-save-as-dialog.ui b/e-util/gal-view-instance-save-as-dialog.ui new file mode 100644 index 0000000000..d7215f1f61 --- /dev/null +++ b/e-util/gal-view-instance-save-as-dialog.ui @@ -0,0 +1,133 @@ +<?xml version="1.0"?> +<interface> + <!-- interface-requires gtk+ 2.12 --> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkVBox" id="vbox-top"> + <property name="visible">True</property> + <property name="border_width">5</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkRadioButton" id="radiobutton-create"> + <property name="label" translatable="yes">_Create new view</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="left_padding">24</property> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Name:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry-create</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry-create"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkRadioButton" id="radiobutton-replace"> + <property name="label" translatable="yes">_Replace existing view</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">radiobutton-create</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="left_padding">24</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="custom-replace"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> +</interface> diff --git a/e-util/gal-view-instance.c b/e-util/gal-view-instance.c new file mode 100644 index 0000000000..e0a107f146 --- /dev/null +++ b/e-util/gal-view-instance.c @@ -0,0 +1,502 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-view-instance.h" + +#include <ctype.h> +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <sys/stat.h> + +#include <gtk/gtk.h> +#include <libxml/parser.h> +#include <glib/gstdio.h> +#include <glib/gi18n.h> + +#include <libedataserver/libedataserver.h> + +#include "e-unicode.h" +#include "e-xml-utils.h" +#include "gal-define-views-dialog.h" +#include "gal-view-instance-save-as-dialog.h" + +G_DEFINE_TYPE (GalViewInstance, gal_view_instance, G_TYPE_OBJECT) + +#define d(x) + +enum { + DISPLAY_VIEW, + CHANGED, + LOADED, + LAST_SIGNAL +}; + +static guint gal_view_instance_signals[LAST_SIGNAL] = { 0, }; + +static void +gal_view_instance_changed (GalViewInstance *instance) +{ + g_return_if_fail (instance != NULL); + g_return_if_fail (GAL_IS_VIEW_INSTANCE (instance)); + + g_signal_emit ( + instance, + gal_view_instance_signals[CHANGED], 0); +} + +static void +gal_view_instance_display_view (GalViewInstance *instance, + GalView *view) +{ + g_return_if_fail (instance != NULL); + g_return_if_fail (GAL_IS_VIEW_INSTANCE (instance)); + + g_signal_emit ( + instance, + gal_view_instance_signals[DISPLAY_VIEW], 0, + view); +} + +static void +save_current_view (GalViewInstance *instance) +{ + xmlDoc *doc; + xmlNode *root; + + doc = xmlNewDoc ((const guchar *)"1.0"); + root = xmlNewNode (NULL, (const guchar *)"GalViewCurrentView"); + xmlDocSetRootElement (doc, root); + + if (instance->current_id) + e_xml_set_string_prop_by_name (root, (const guchar *)"current_view", instance->current_id); + if (instance->current_type) + e_xml_set_string_prop_by_name (root, (const guchar *)"current_view_type", instance->current_type); + + if (e_xml_save_file (instance->current_view_filename, doc) == -1) + g_warning ("Unable to save view to %s - %s", instance->current_view_filename, g_strerror (errno)); + xmlFreeDoc (doc); +} + +static void +view_changed (GalView *view, + GalViewInstance *instance) +{ + if (instance->current_id != NULL) { + g_free (instance->current_id); + instance->current_id = NULL; + save_current_view (instance); + gal_view_instance_changed (instance); + } + + gal_view_save (view, instance->custom_filename); +} + +static void +disconnect_view (GalViewInstance *instance) +{ + if (instance->current_view) { + if (instance->view_changed_id) { + g_signal_handler_disconnect ( + instance->current_view, + instance->view_changed_id); + } + + g_object_unref (instance->current_view); + } + g_free (instance->current_type); + g_free (instance->current_title); + instance->current_title = NULL; + instance->current_type = NULL; + instance->view_changed_id = 0; + instance->current_view = NULL; +} + +static void +connect_view (GalViewInstance *instance, + GalView *view) +{ + if (instance->current_view) + disconnect_view (instance); + instance->current_view = view; + + instance->current_title = g_strdup (gal_view_get_title (view)); + instance->current_type = g_strdup (gal_view_get_type_code (view)); + instance->view_changed_id = g_signal_connect ( + instance->current_view, "changed", + G_CALLBACK (view_changed), instance); + + gal_view_instance_display_view (instance, instance->current_view); +} + +static void +gal_view_instance_dispose (GObject *object) +{ + GalViewInstance *instance = GAL_VIEW_INSTANCE (object); + + if (instance->collection) { + if (instance->collection_changed_id) { + g_signal_handler_disconnect ( + instance->collection, + instance->collection_changed_id); + } + g_object_unref (instance->collection); + } + + g_free (instance->instance_id); + g_free (instance->custom_filename); + g_free (instance->current_view_filename); + + g_free (instance->current_id); + disconnect_view (instance); + + g_free (instance->default_view); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (gal_view_instance_parent_class)->dispose (object); +} + +static void +gal_view_instance_class_init (GalViewInstanceClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->dispose = gal_view_instance_dispose; + + gal_view_instance_signals[DISPLAY_VIEW] = g_signal_new ( + "display_view", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GalViewInstanceClass, display_view), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + GAL_TYPE_VIEW); + + gal_view_instance_signals[CHANGED] = g_signal_new ( + "changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GalViewInstanceClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gal_view_instance_signals[LOADED] = g_signal_new ( + "loaded", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (GalViewInstanceClass, loaded), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + class->display_view = NULL; + class->changed = NULL; +} + +static void +gal_view_instance_init (GalViewInstance *instance) +{ + instance->collection = NULL; + + instance->instance_id = NULL; + instance->custom_filename = NULL; + instance->current_view_filename = NULL; + + instance->current_title = NULL; + instance->current_type = NULL; + instance->current_id = NULL; + instance->current_view = NULL; + + instance->view_changed_id = 0; + instance->collection_changed_id = 0; + + instance->loaded = FALSE; + instance->default_view = NULL; +} + +static void +collection_changed (GalView *view, + GalViewInstance *instance) +{ + if (instance->current_id) { + gchar *view_id = instance->current_id; + instance->current_id = NULL; + gal_view_instance_set_current_view_id (instance, view_id); + g_free (view_id); + } +} + +static void +load_current_view (GalViewInstance *instance) +{ + xmlDoc *doc = NULL; + xmlNode *root; + GalView *view = NULL; + + if (g_file_test (instance->current_view_filename, G_FILE_TEST_IS_REGULAR)) { +#ifdef G_OS_WIN32 + gchar *locale_filename = g_win32_locale_filename_from_utf8 (instance->current_view_filename); + if (locale_filename != NULL) + doc = xmlParseFile (locale_filename); + g_free (locale_filename); +#else + doc = xmlParseFile (instance->current_view_filename); +#endif + } + + if (doc == NULL) { + instance->current_id = g_strdup (gal_view_instance_get_default_view (instance)); + + if (instance->current_id) { + gint index = gal_view_collection_get_view_index_by_id ( + instance->collection, + instance->current_id); + + if (index != -1) { + view = gal_view_collection_get_view ( + instance->collection, index); + view = gal_view_clone (view); + connect_view (instance, view); + } + } + return; + } + + root = xmlDocGetRootElement (doc); + instance->current_id = e_xml_get_string_prop_by_name_with_default (root, (const guchar *)"current_view", NULL); + + if (instance->current_id != NULL) { + gint index = gal_view_collection_get_view_index_by_id ( + instance->collection, + instance->current_id); + + if (index != -1) { + view = gal_view_collection_get_view ( + instance->collection, index); + view = gal_view_clone (view); + } + } + if (view == NULL) { + gchar *type; + type = e_xml_get_string_prop_by_name_with_default (root, (const guchar *)"current_view_type", NULL); + view = gal_view_collection_load_view_from_file ( + instance->collection, type, + instance->custom_filename); + g_free (type); + } + + connect_view (instance, view); + + xmlFreeDoc (doc); +} + +/** + * gal_view_instance_new: + * @collection: This %GalViewCollection should be loaded before being passed to this function. + * @instance_id: Which instance of this type of object is this (for most of evo, this is the folder id.) + * + * Create a new %GalViewInstance. + * + * Return value: The new %GalViewInstance. + **/ +GalViewInstance * +gal_view_instance_new (GalViewCollection *collection, + const gchar *instance_id) +{ + GalViewInstance *instance = g_object_new (GAL_VIEW_INSTANCE_TYPE, NULL); + if (gal_view_instance_construct (instance, collection, instance_id)) + return instance; + else { + g_object_unref (instance); + return NULL; + } +} + +GalViewInstance * +gal_view_instance_construct (GalViewInstance *instance, + GalViewCollection *collection, + const gchar *instance_id) +{ + gchar *filename; + gchar *safe_id; + + g_return_val_if_fail (gal_view_collection_loaded (collection), NULL); + + instance->collection = collection; + if (collection) + g_object_ref (collection); + instance->collection_changed_id = g_signal_connect ( + collection, "changed", + G_CALLBACK (collection_changed), instance); + + if (instance_id) + instance->instance_id = g_strdup (instance_id); + else + instance->instance_id = g_strdup (""); + + safe_id = g_strdup (instance->instance_id); + e_filename_make_safe (safe_id); + + filename = g_strdup_printf ("custom_view-%s.xml", safe_id); + instance->custom_filename = g_build_filename (instance->collection->local_dir, filename, NULL); + g_free (filename); + + filename = g_strdup_printf ("current_view-%s.xml", safe_id); + instance->current_view_filename = g_build_filename (instance->collection->local_dir, filename, NULL); + g_free (filename); + + g_free (safe_id); + + return instance; +} + +/* Manipulate the current view. */ +gchar * +gal_view_instance_get_current_view_id (GalViewInstance *instance) +{ + if (instance->current_id && gal_view_collection_get_view_index_by_id (instance->collection, instance->current_id) != -1) + return g_strdup (instance->current_id); + else + return NULL; +} + +void +gal_view_instance_set_current_view_id (GalViewInstance *instance, + const gchar *view_id) +{ + GalView *view; + gint index; + + g_return_if_fail (instance != NULL); + g_return_if_fail (GAL_IS_VIEW_INSTANCE (instance)); + + d (g_print ("%s: view_id set to %s\n", G_STRFUNC, view_id)); + + if (instance->current_id && !strcmp (instance->current_id, view_id)) + return; + + g_free (instance->current_id); + instance->current_id = g_strdup (view_id); + + index = gal_view_collection_get_view_index_by_id (instance->collection, view_id); + if (index != -1) { + view = gal_view_collection_get_view (instance->collection, index); + connect_view (instance, gal_view_clone (view)); + } + + if (instance->loaded) + save_current_view (instance); + gal_view_instance_changed (instance); +} + +GalView * +gal_view_instance_get_current_view (GalViewInstance *instance) +{ + return instance->current_view; +} + +void +gal_view_instance_set_custom_view (GalViewInstance *instance, + GalView *view) +{ + g_free (instance->current_id); + instance->current_id = NULL; + + view = gal_view_clone (view); + connect_view (instance, view); + gal_view_save (view, instance->custom_filename); + save_current_view (instance); + gal_view_instance_changed (instance); +} + +static void +dialog_response (GtkWidget *dialog, + gint id, + GalViewInstance *instance) +{ + if (id == GTK_RESPONSE_OK) { + gal_view_instance_save_as_dialog_save (GAL_VIEW_INSTANCE_SAVE_AS_DIALOG (dialog)); + } + gtk_widget_destroy (dialog); +} + +void +gal_view_instance_save_as (GalViewInstance *instance) +{ + GtkWidget *dialog; + + g_return_if_fail (instance != NULL); + + dialog = gal_view_instance_save_as_dialog_new (instance); + g_signal_connect ( + dialog, "response", + G_CALLBACK (dialog_response), instance); + gtk_widget_show (dialog); +} + +/* This is idempotent. Once it's been called once, the rest of the calls are ignored. */ +void +gal_view_instance_load (GalViewInstance *instance) +{ + if (!instance->loaded) { + load_current_view (instance); + instance->loaded = TRUE; + g_signal_emit (instance, gal_view_instance_signals[LOADED], 0); + } +} + +/* These only mean anything before gal_view_instance_load is called the first time. */ +const gchar * +gal_view_instance_get_default_view (GalViewInstance *instance) +{ + if (instance->default_view) + return instance->default_view; + else + return gal_view_collection_get_default_view (instance->collection); +} + +void +gal_view_instance_set_default_view (GalViewInstance *instance, + const gchar *id) +{ + g_free (instance->default_view); + instance->default_view = g_strdup (id); +} + +gboolean +gal_view_instance_exists (GalViewInstance *instance) +{ + struct stat st; + + if (instance->current_view_filename && g_stat (instance->current_view_filename, &st) == 0 && st.st_size > 0 && S_ISREG (st.st_mode)) + return TRUE; + else + return FALSE; + +} diff --git a/e-util/gal-view-instance.h b/e-util/gal-view-instance.h new file mode 100644 index 0000000000..c5debd1c3a --- /dev/null +++ b/e-util/gal-view-instance.h @@ -0,0 +1,114 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef _GAL_VIEW_INSTANCE_H_ +#define _GAL_VIEW_INSTANCE_H_ + +#include <e-util/gal-view-collection.h> + +G_BEGIN_DECLS + +#define GAL_VIEW_INSTANCE_TYPE (gal_view_instance_get_type ()) +#define GAL_VIEW_INSTANCE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GAL_VIEW_INSTANCE_TYPE, GalViewInstance)) +#define GAL_VIEW_INSTANCE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GAL_VIEW_INSTANCE_TYPE, GalViewInstanceClass)) +#define GAL_IS_VIEW_INSTANCE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GAL_VIEW_INSTANCE_TYPE)) +#define GAL_IS_VIEW_INSTANCE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GAL_VIEW_INSTANCE_TYPE)) + +typedef struct { + GObject base; + + GalViewCollection *collection; + + gchar *instance_id; + gchar *current_view_filename; + gchar *custom_filename; + + gchar *current_title; + gchar *current_type; + gchar *current_id; + + GalView *current_view; + + guint view_changed_id; + guint collection_changed_id; + + guint loaded : 1; + gchar *default_view; +} GalViewInstance; + +typedef struct { + GObjectClass parent_class; + + /* + * Signals + */ + void (*display_view) (GalViewInstance *instance, + GalView *view); + void (*changed) (GalViewInstance *instance); + void (*loaded) (GalViewInstance *instance); +} GalViewInstanceClass; + +/* Standard functions */ +GType gal_view_instance_get_type (void); + +/* */ +/*collection should be loaded when you call this. + instance_id: Which instance of this type of object is this (for most of evo, this is the folder id.) */ +GalViewInstance *gal_view_instance_new (GalViewCollection *collection, + const gchar *instance_id); +GalViewInstance *gal_view_instance_construct (GalViewInstance *instance, + GalViewCollection *collection, + const gchar *instance_id); + +/* Manipulate the current view. */ +gchar *gal_view_instance_get_current_view_id (GalViewInstance *instance); +void gal_view_instance_set_current_view_id (GalViewInstance *instance, + const gchar *view_id); +GalView *gal_view_instance_get_current_view (GalViewInstance *instance); + +/* Sets the current view to the given custom view. */ +void gal_view_instance_set_custom_view (GalViewInstance *instance, + GalView *view); + +/* Returns true if this instance has ever been used before. */ +gboolean gal_view_instance_exists (GalViewInstance *instance); + +/* Manipulate the view collection */ +/* void gal_view_instance_set_as_default (GalViewInstance *instance); */ +void gal_view_instance_save_as (GalViewInstance *instance); + +/* This is idempotent. Once it's been called once, the rest of the calls are ignored. */ +void gal_view_instance_load (GalViewInstance *instance); + +/* These only mean anything before gal_view_instance_load is called the first time. */ +const gchar *gal_view_instance_get_default_view (GalViewInstance *instance); +void gal_view_instance_set_default_view (GalViewInstance *instance, + const gchar *id); + +G_END_DECLS + +#endif /* _GAL_VIEW_INSTANCE_H_ */ diff --git a/e-util/gal-view-new-dialog.c b/e-util/gal-view-new-dialog.c new file mode 100644 index 0000000000..1df95a1985 --- /dev/null +++ b/e-util/gal-view-new-dialog.c @@ -0,0 +1,291 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-view-new-dialog.h" + +#include <glib/gi18n.h> + +#include "e-misc-utils.h" +#include "e-util-private.h" +#include "e-unicode.h" +#include "gal-define-views-model.h" + +enum { + PROP_0, + PROP_NAME, + PROP_FACTORY +}; + +G_DEFINE_TYPE (GalViewNewDialog, gal_view_new_dialog, GTK_TYPE_DIALOG) + +static void +gal_view_new_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GalViewNewDialog *dialog; + GtkWidget *entry; + + dialog = GAL_VIEW_NEW_DIALOG (object); + + switch (property_id) { + case PROP_NAME: + entry = e_builder_get_widget (dialog->builder, "entry-name"); + if (entry && GTK_IS_ENTRY (entry)) { + gtk_entry_set_text (GTK_ENTRY (entry), g_value_get_string (value)); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + return; + } +} + +static void +gal_view_new_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GalViewNewDialog *dialog; + GtkWidget *entry; + + dialog = GAL_VIEW_NEW_DIALOG (object); + + switch (property_id) { + case PROP_NAME: + entry = e_builder_get_widget (dialog->builder, "entry-name"); + if (entry && GTK_IS_ENTRY (entry)) { + g_value_set_string (value, gtk_entry_get_text (GTK_ENTRY (entry))); + } + break; + case PROP_FACTORY: + g_value_set_object (value, dialog->selected_factory); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gal_view_new_dialog_dispose (GObject *object) +{ + GalViewNewDialog *gal_view_new_dialog = GAL_VIEW_NEW_DIALOG (object); + + if (gal_view_new_dialog->builder) + g_object_unref (gal_view_new_dialog->builder); + gal_view_new_dialog->builder = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (gal_view_new_dialog_parent_class)->dispose (object); +} + +static void +gal_view_new_dialog_class_init (GalViewNewDialogClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = gal_view_new_dialog_set_property; + object_class->get_property = gal_view_new_dialog_get_property; + object_class->dispose = gal_view_new_dialog_dispose; + + g_object_class_install_property ( + object_class, + PROP_NAME, + g_param_spec_string ( + "name", + "Name", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_FACTORY, + g_param_spec_object ( + "factory", + "Factory", + NULL, + GAL_TYPE_VIEW_FACTORY, + G_PARAM_READWRITE)); +} + +static void +gal_view_new_dialog_init (GalViewNewDialog *dialog) +{ + GtkWidget *content_area; + GtkWidget *parent; + GtkWidget *widget; + + dialog->builder = gtk_builder_new (); + e_load_ui_builder_definition ( + dialog->builder, "gal-view-new-dialog.ui"); + + widget = e_builder_get_widget (dialog->builder, "table-top"); + if (!widget) { + return; + } + + g_object_ref (widget); + + parent = gtk_widget_get_parent (widget); + gtk_container_remove (GTK_CONTAINER (parent), widget); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_box_pack_start (GTK_BOX (content_area), widget, TRUE, TRUE, 0); + + g_object_unref (widget); + + gtk_dialog_add_buttons ( + GTK_DIALOG (dialog), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, NULL); + + gtk_window_set_resizable (GTK_WINDOW (dialog), TRUE); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_window_set_title (GTK_WINDOW (dialog), _("Define New View")); + + dialog->collection = NULL; + dialog->selected_factory = NULL; +} + +GtkWidget * +gal_view_new_dialog_new (GalViewCollection *collection) +{ + GtkWidget *widget = + gal_view_new_dialog_construct ( + g_object_new (GAL_VIEW_NEW_DIALOG_TYPE, NULL), + collection); + return widget; +} + +static void +sensitize_ok_response (GalViewNewDialog *dialog) +{ + gboolean ok = TRUE; + const gchar *text; + + text = gtk_entry_get_text (GTK_ENTRY (dialog->entry)); + if (!text || !text[0]) + ok = FALSE; + + if (!dialog->selected_factory) + ok = FALSE; + + gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog), GTK_RESPONSE_OK, ok); +} + +static gboolean +selection_func (GtkTreeSelection *selection, + GtkTreeModel *model, + GtkTreePath *path, + gboolean path_currently_selected, + gpointer data) +{ + GtkTreeIter iter; + GalViewNewDialog *dialog = data; + + if (path_currently_selected) + return TRUE; + + model = GTK_TREE_MODEL (dialog->list_store); + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, 1, &dialog->selected_factory, -1); + + sensitize_ok_response (dialog); + + return TRUE; +} + +static void +entry_changed (GtkWidget *entry, + gpointer data) +{ + GalViewNewDialog *dialog = data; + + sensitize_ok_response (dialog); +} + +GtkWidget * +gal_view_new_dialog_construct (GalViewNewDialog *dialog, + GalViewCollection *collection) +{ + GList *iterator; + GtkTreeSelection *selection; + GtkTreeViewColumn *column; + GtkCellRenderer *rend; + + dialog->collection = collection; + dialog->list = e_builder_get_widget (dialog->builder,"list-type-list"); + dialog->entry = e_builder_get_widget (dialog->builder, "entry-name"); + + dialog->list_store = gtk_list_store_new ( + 2, G_TYPE_STRING, G_TYPE_POINTER); + + rend = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes ( + "factory title", rend, "text", 0, NULL); + + gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->list), column); + + iterator = dialog->collection->factory_list; + for (; iterator; iterator = g_list_next (iterator)) { + GalViewFactory *factory = iterator->data; + GtkTreeIter iter; + + g_object_ref (factory); + gtk_list_store_append ( + dialog->list_store, &iter); + gtk_list_store_set ( + dialog->list_store, &iter, + 0, gal_view_factory_get_title (factory), + 1, factory, + -1); + } + + gtk_tree_view_set_model ( + GTK_TREE_VIEW (dialog->list), + GTK_TREE_MODEL (dialog->list_store)); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->list)); + + gtk_tree_selection_set_select_function ( + selection, selection_func, dialog, NULL); + + g_signal_connect ( + dialog->entry, "changed", + G_CALLBACK (entry_changed), dialog); + + sensitize_ok_response (dialog); + + return GTK_WIDGET (dialog); +} + diff --git a/e-util/gal-view-new-dialog.h b/e-util/gal-view-new-dialog.h new file mode 100644 index 0000000000..503a594abb --- /dev/null +++ b/e-util/gal-view-new-dialog.h @@ -0,0 +1,81 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef __GAL_VIEW_NEW_DIALOG_H__ +#define __GAL_VIEW_NEW_DIALOG_H__ + +#include <gtk/gtk.h> +#include <e-util/gal-view-collection.h> + +G_BEGIN_DECLS + +/* GalViewNewDialog - A dialog displaying information about a contact. + * + * The following arguments are available: + * + * name type read/write description + * -------------------------------------------------------------------------------- + */ + +#define GAL_VIEW_NEW_DIALOG_TYPE (gal_view_new_dialog_get_type ()) +#define GAL_VIEW_NEW_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GAL_VIEW_NEW_DIALOG_TYPE, GalViewNewDialog)) +#define GAL_VIEW_NEW_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GAL_VIEW_NEW_DIALOG_TYPE, GalViewNewDialogClass)) +#define GAL_IS_VIEW_NEW_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAL_VIEW_NEW_DIALOG_TYPE)) +#define GAL_IS_VIEW_NEW_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), GAL_VIEW_NEW_DIALOG_TYPE)) + +typedef struct _GalViewNewDialog GalViewNewDialog; +typedef struct _GalViewNewDialogClass GalViewNewDialogClass; + +struct _GalViewNewDialog +{ + GtkDialog parent; + + /* item specific fields */ + GtkBuilder *builder; + + GalViewCollection *collection; + GalViewFactory *selected_factory; + + GtkListStore *list_store; + + GtkWidget *entry; + GtkWidget *list; +}; + +struct _GalViewNewDialogClass +{ + GtkDialogClass parent_class; +}; + +GtkWidget *gal_view_new_dialog_new (GalViewCollection *collection); +GType gal_view_new_dialog_get_type (void); + +GtkWidget *gal_view_new_dialog_construct (GalViewNewDialog *dialog, + GalViewCollection *collection); + +G_END_DECLS + +#endif /* __GAL_VIEW_NEW_DIALOG_H__ */ diff --git a/e-util/gal-view-new-dialog.ui b/e-util/gal-view-new-dialog.ui new file mode 100644 index 0000000000..227e3954d8 --- /dev/null +++ b/e-util/gal-view-new-dialog.ui @@ -0,0 +1,177 @@ +<?xml version="1.0"?> +<!--*- mode: xml -*--> +<interface> + <object class="GtkDialog" id="dialog1"> + <property name="title" translatable="yes"/> + <property name="type">GTK_WINDOW_TOPLEVEL</property> + <property name="window_position">GTK_WIN_POS_NONE</property> + <property name="modal">False</property> + <property name="resizable">True</property> + <property name="destroy_with_parent">False</property> + <property name="decorated">True</property> + <property name="skip_taskbar_hint">False</property> + <property name="skip_pager_hint">False</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property> + <property name="gravity">GDK_GRAVITY_NORTH_WEST</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">8</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">GTK_BUTTONBOX_END</property> + <child> + <object class="GtkButton" id="button1"> + <property name="visible">True</property> + <property name="can_default">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-ok</property> + <property name="use_stock">True</property> + <property name="relief">GTK_RELIEF_NORMAL</property> + <property name="focus_on_click">True</property> + </object> + </child> + <child> + <object class="GtkButton" id="button3"> + <property name="visible">True</property> + <property name="can_default">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-cancel</property> + <property name="use_stock">True</property> + <property name="relief">GTK_RELIEF_NORMAL</property> + <property name="focus_on_click">True</property> + </object> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table-top"> + <property name="visible">True</property> + <property name="n_rows">4</property> + <property name="n_columns">1</property> + <property name="homogeneous">False</property> + <property name="row_spacing">6</property> + <property name="column_spacing">6</property> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Name of new view:</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_CENTER</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="mnemonic_widget">entry-name</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="right_attach">1</property> + <property name="top_attach">0</property> + <property name="bottom_attach">1</property> + <property name="x_options">fill</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry-name"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="editable">True</property> + <property name="visibility">True</property> + <property name="max_length">0</property> + <property name="text" translatable="yes"/> + <property name="has_frame">True</property> + <property name="activates_default">False</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="right_attach">1</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">fill</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="label" translatable="yes">Type of view:</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_CENTER</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="right_attach">1</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">fill</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="window_placement">GTK_CORNER_TOP_LEFT</property> + <child> + <object class="GtkTreeView" id="list-type-list"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + <property name="rules_hint">False</property> + <property name="reorderable">False</property> + <property name="enable_search">True</property> + <accessibility> + + </accessibility> + <child internal-child="accessible"> + <object class="AtkObject" id="a11y-list-type-list1"> + <property name="AtkObject::accessible_name" translatable="yes">Type of View</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="right_attach">1</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">fill</property> + </packing> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button1</action-widget> + <action-widget response="0">button3</action-widget> + </action-widgets> + </object> +</interface> diff --git a/e-util/gal-view.c b/e-util/gal-view.c new file mode 100644 index 0000000000..4302988a6e --- /dev/null +++ b/e-util/gal-view.c @@ -0,0 +1,280 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gal-view.h" + +#define d(x) + +enum { + PROP_0, + PROP_TITLE, + PROP_TYPE_CODE +}; + +enum { + CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_ABSTRACT_TYPE (GalView, gal_view, G_TYPE_OBJECT) + +static void +view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_TITLE: + gal_view_set_title ( + GAL_VIEW (object), + g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_TITLE: + g_value_set_string ( + value, gal_view_get_title ( + GAL_VIEW (object))); + return; + + case PROP_TYPE_CODE: + g_value_set_string ( + value, gal_view_get_type_code ( + GAL_VIEW (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +gal_view_class_init (GalViewClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = view_set_property; + object_class->get_property = view_get_property; + + g_object_class_install_property ( + object_class, + PROP_TITLE, + g_param_spec_string ( + "title", + NULL, + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_TYPE_CODE, + g_param_spec_string ( + "type-code", + NULL, + NULL, + NULL, + G_PARAM_READABLE)); + + signals[CHANGED] = g_signal_new ( + "changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GalViewClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +gal_view_init (GalView *view) +{ +} + +/** + * gal_view_edit + * @view: The view to edit + * @parent: the parent window. + */ +void +gal_view_edit (GalView *view, + GtkWindow *parent) +{ + GalViewClass *class; + + g_return_if_fail (GAL_IS_VIEW (view)); + g_return_if_fail (GTK_IS_WINDOW (parent)); + + class = GAL_VIEW_GET_CLASS (view); + g_return_if_fail (class->edit != NULL); + + class->edit (view, parent); +} + +/** + * gal_view_load + * @view: The view to load to + * @filename: The file to load from + */ +void +gal_view_load (GalView *view, + const gchar *filename) +{ + GalViewClass *class; + + g_return_if_fail (GAL_IS_VIEW (view)); + g_return_if_fail (filename != NULL); + + class = GAL_VIEW_GET_CLASS (view); + g_return_if_fail (class->load != NULL); + + class->load (view, filename); +} + +/** + * gal_view_save + * @view: The view to save + * @filename: The file to save to + */ +void +gal_view_save (GalView *view, + const gchar *filename) +{ + GalViewClass *class; + + g_return_if_fail (GAL_IS_VIEW (view)); + g_return_if_fail (filename != NULL); + + class = GAL_VIEW_GET_CLASS (view); + g_return_if_fail (class->save != NULL); + + class->save (view, filename); +} + +/** + * gal_view_get_title + * @view: The view to query. + * + * Returns: The title of the view. + */ +const gchar * +gal_view_get_title (GalView *view) +{ + GalViewClass *class; + + g_return_val_if_fail (GAL_IS_VIEW (view), NULL); + + class = GAL_VIEW_GET_CLASS (view); + g_return_val_if_fail (class->get_title != NULL, NULL); + + return class->get_title (view); +} + +/** + * gal_view_set_title + * @view: The view to set. + * @title: The new title value. + */ +void +gal_view_set_title (GalView *view, + const gchar *title) +{ + GalViewClass *class; + + g_return_if_fail (GAL_IS_VIEW (view)); + + class = GAL_VIEW_GET_CLASS (view); + g_return_if_fail (class->set_title != NULL); + + class->set_title (view, title); + + g_object_notify (G_OBJECT (view), "title"); +} + +/** + * gal_view_get_type_code + * @view: The view to get. + * + * Returns: The type of the view. + */ +const gchar * +gal_view_get_type_code (GalView *view) +{ + GalViewClass *class; + + g_return_val_if_fail (GAL_IS_VIEW (view), NULL); + + class = GAL_VIEW_GET_CLASS (view); + g_return_val_if_fail (class->get_type_code != NULL, NULL); + + return class->get_type_code (view); +} + +/** + * gal_view_clone + * @view: The view to clone. + * + * Returns: The clone. + */ +GalView * +gal_view_clone (GalView *view) +{ + GalViewClass *class; + + g_return_val_if_fail (GAL_IS_VIEW (view), NULL); + + class = GAL_VIEW_GET_CLASS (view); + g_return_val_if_fail (class->clone != NULL, NULL); + + return class->clone (view); +} + +/** + * gal_view_changed + * @view: The view that changed. + */ +void +gal_view_changed (GalView *view) +{ + g_return_if_fail (GAL_IS_VIEW (view)); + + g_signal_emit (view, signals[CHANGED], 0); +} + diff --git a/e-util/gal-view.h b/e-util/gal-view.h new file mode 100644 index 0000000000..d769895d03 --- /dev/null +++ b/e-util/gal-view.h @@ -0,0 +1,98 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Chris Lahey <clahey@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef GAL_VIEW_H +#define GAL_VIEW_H + +#include <gtk/gtk.h> +#include <libxml/tree.h> + +/* Standard GObject macros */ +#define GAL_TYPE_VIEW \ + (gal_view_get_type ()) +#define GAL_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), GAL_TYPE_VIEW, GalView)) +#define GAL_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), GAL_TYPE_VIEW, GalViewClass)) +#define GAL_IS_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), GAL_TYPE_VIEW)) +#define GAL_IS_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), GAL_TYPE_VIEW)) +#define GAL_VIEW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), GAL_TYPE_VIEW, GalViewClass)) + +G_BEGIN_DECLS + +typedef struct _GalView GalView; +typedef struct _GalViewClass GalViewClass; + +struct _GalView { + GObject parent; +}; + +struct _GalViewClass { + GObjectClass parent_class; + + /* Methods */ + void (*edit) (GalView *view, + GtkWindow *parent_window); + void (*load) (GalView *view, + const gchar *filename); + void (*save) (GalView *view, + const gchar *filename); + const gchar * (*get_title) (GalView *view); + void (*set_title) (GalView *view, + const gchar *title); + const gchar * (*get_type_code) (GalView *view); + GalView * (*clone) (GalView *view); + + /* Signals */ + void (*changed) (GalView *view); +}; + +GType gal_view_get_type (void); +void gal_view_edit (GalView *view, + GtkWindow *parent); +void gal_view_load (GalView *view, + const gchar *filename); +void gal_view_save (GalView *view, + const gchar *filename); +const gchar * gal_view_get_title (GalView *view); +void gal_view_set_title (GalView *view, + const gchar *title); +const gchar * gal_view_get_type_code (GalView *view); +GalView * gal_view_clone (GalView *view); +void gal_view_changed (GalView *view); + +G_END_DECLS + +#endif /* GAL_VIEW_H */ diff --git a/e-util/test-calendar.c b/e-util/test-calendar.c new file mode 100644 index 0000000000..718c80e639 --- /dev/null +++ b/e-util/test-calendar.c @@ -0,0 +1,145 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * test-calendar - tests the ECalendar widget. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> +#include <e-util/e-util.h> + +/* Drag and Drop stuff. */ +enum { + TARGET_SHORTCUT +}; + +static GtkTargetEntry target_table[] = { + { (gchar *) "E-SHORTCUT", 0, TARGET_SHORTCUT } +}; + +static void on_date_range_changed (ECalendarItem *calitem); +static void on_selection_changed (ECalendarItem *calitem); + +static void +delete_event_cb (GtkWidget *widget, + GdkEventAny *event, + gpointer data) +{ + gtk_main_quit (); +} + +gint +main (gint argc, + gchar **argv) +{ + GtkWidget *window; + GtkWidget *cal; + GtkWidget *vbox; + ECalendarItem *calitem; + + gtk_init (&argc, &argv); + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title (GTK_WINDOW (window), "ECalendar Test"); + gtk_window_set_default_size (GTK_WINDOW (window), 400, 400); + gtk_window_set_resizable (GTK_WINDOW (window), TRUE); + gtk_container_set_border_width (GTK_CONTAINER (window), 8); + + g_signal_connect ( + window, "delete_event", + G_CALLBACK (delete_event_cb), NULL); + + cal = e_calendar_new (); + e_calendar_set_minimum_size (E_CALENDAR (cal), 1, 1); + calitem = E_CALENDAR (cal)->calitem; + gtk_widget_show (cal); + + g_signal_connect ( + calitem, "date_range_changed", + G_CALLBACK (on_date_range_changed), NULL); + g_signal_connect ( + calitem, "selection_changed", + G_CALLBACK (on_selection_changed), NULL); + + gtk_drag_dest_set ( + cal, + GTK_DEST_DEFAULT_ALL, + target_table, G_N_ELEMENTS (target_table), + GDK_ACTION_COPY | GDK_ACTION_MOVE); + + vbox = gtk_vbox_new (FALSE, 0); + gtk_box_pack_start (GTK_BOX (vbox), cal, TRUE, TRUE, 0); + gtk_widget_show (vbox); + + gtk_container_add (GTK_CONTAINER (window), vbox); + gtk_widget_show (window); + + gtk_main (); + + return 0; +} + +static void +on_date_range_changed (ECalendarItem *calitem) +{ + gint start_year, start_month, start_day; + gint end_year, end_month, end_day; + + e_calendar_item_get_date_range ( + calitem, + &start_year, &start_month, &start_day, + &end_year, &end_month, &end_day); + + g_print ( + "Date range changed (D/M/Y): %i/%i/%i - %i/%i/%i\n", + start_day, start_month + 1, start_year, + end_day, end_month + 1, end_year); + + /* These days should windowear bold. Remember month is 0 to 11. */ + e_calendar_item_mark_day ( + calitem, 2000, 7, 26, /* 26th Aug 2000. */ + E_CALENDAR_ITEM_MARK_BOLD, FALSE); + e_calendar_item_mark_day ( + calitem, 2000, 8, 13, /* 13th Sep 2000. */ + E_CALENDAR_ITEM_MARK_BOLD, FALSE); +} + +static void +on_selection_changed (ECalendarItem *calitem) +{ + GDate start_date, end_date; + + e_calendar_item_get_selection (calitem, &start_date, &end_date); + + g_print ( + "Selection changed (D/M/Y): %i/%i/%i - %i/%i/%i\n", + g_date_get_day (&start_date), + g_date_get_month (&start_date), + g_date_get_year (&start_date), + g_date_get_day (&end_date), + g_date_get_month (&end_date), + g_date_get_year (&end_date)); +} diff --git a/e-util/test-category-completion.c b/e-util/test-category-completion.c new file mode 100644 index 0000000000..d9e14731e1 --- /dev/null +++ b/e-util/test-category-completion.c @@ -0,0 +1,67 @@ +/* + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include <e-util/e-util.h> + +static gboolean +on_idle_create_widget (void) +{ + GtkWidget *window; + GtkWidget *vgrid; + GtkWidget *entry; + GtkEntryCompletion *completion; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_default_size (GTK_WINDOW (window), 400, 200); + + g_signal_connect ( + window, "delete-event", + G_CALLBACK (gtk_main_quit), NULL); + + vgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_VERTICAL, + "column-homogeneous", FALSE, + "row-spacing", 3, + NULL); + gtk_container_add (GTK_CONTAINER (window), vgrid); + + entry = gtk_entry_new (); + completion = e_category_completion_new (); + gtk_entry_set_completion (GTK_ENTRY (entry), completion); + gtk_widget_set_vexpand (entry, TRUE); + gtk_widget_set_hexpand (entry, TRUE); + gtk_widget_set_halign (entry, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (vgrid), entry); + + gtk_widget_show_all (window); + + return FALSE; +} + +gint +main (gint argc, + gchar **argv) +{ + gtk_init (&argc, &argv); + + g_idle_add ((GSourceFunc) on_idle_create_widget, NULL); + + gtk_main (); + + return 0; +} diff --git a/e-util/test-contact-store.c b/e-util/test-contact-store.c new file mode 100644 index 0000000000..59ba42502b --- /dev/null +++ b/e-util/test-contact-store.c @@ -0,0 +1,145 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* test-contact-store.c - Test program for EContactStore. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Hans Petter Jansson <hpj@novell.com> + */ + +#include <e-util/e-util.h> + +static void +entry_changed (GtkWidget *entry, + EContactStore *contact_store) +{ + const gchar *text; + EBookQuery *query; + + text = gtk_entry_get_text (GTK_ENTRY (entry)); + + query = e_book_query_any_field_contains (text); + e_contact_store_set_query (contact_store, query); + e_book_query_unref (query); +} + +static GtkTreeViewColumn * +create_text_column_for_field (EContactField field_id) +{ + GtkTreeViewColumn *column; + GtkCellRenderer *cell_renderer; + + column = gtk_tree_view_column_new (); + cell_renderer = GTK_CELL_RENDERER (gtk_cell_renderer_text_new ()); + gtk_tree_view_column_pack_start (column, cell_renderer, TRUE); + gtk_tree_view_column_set_resizable (column, TRUE); + gtk_tree_view_column_set_title (column, e_contact_pretty_name (field_id)); + gtk_tree_view_column_add_attribute (column, cell_renderer, "text", field_id); + gtk_tree_view_column_set_sort_column_id (column, field_id); + + return column; +} + +static gint +start_test (const gchar *param) +{ + EContactStore *contact_store; + GtkTreeModel *model_sort; + GtkWidget *scrolled_window; + GtkWidget *window; + GtkWidget *tree_view; + GtkWidget *vgrid; + GtkWidget *entry; + GtkTreeViewColumn *column; +#if 0 /* ACCOUNT_MGMT */ + EBookClient *book_client; +#endif /* ACCOUNT_MGMT */ + EBookQuery *book_query; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + + vgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_VERTICAL, + "column-homogeneous", FALSE, + "row-spacing", 2, + NULL); + gtk_container_add (GTK_CONTAINER (window), vgrid); + + entry = gtk_entry_new (); + gtk_widget_set_halign (entry, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (vgrid), entry); + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_widget_set_hexpand (scrolled_window, TRUE); + gtk_widget_set_halign (scrolled_window, GTK_ALIGN_FILL); + gtk_widget_set_vexpand (scrolled_window, TRUE); + gtk_widget_set_valign (scrolled_window, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (vgrid), scrolled_window); + + contact_store = e_contact_store_new (); + model_sort = gtk_tree_model_sort_new_with_model (GTK_TREE_MODEL (contact_store)); + tree_view = GTK_WIDGET (gtk_tree_view_new ()); + gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view), model_sort); + + column = create_text_column_for_field (E_CONTACT_FILE_AS); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); + + column = create_text_column_for_field (E_CONTACT_FULL_NAME); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); + + column = create_text_column_for_field (E_CONTACT_EMAIL_1); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); + + gtk_container_add (GTK_CONTAINER (scrolled_window), tree_view); + +#if 0 /* ACCOUNT_MGMT */ + book_client = e_book_client_new_default (NULL); + g_warn_if_fail (e_client_open_sync (E_CLIENT (book_client), TRUE, NULL, NULL)); + e_contact_store_add_client (contact_store, book_client); + g_object_unref (book_client); +#endif /* ACCOUNT_MGMT */ + + book_query = e_book_query_any_field_contains (""); + e_contact_store_set_query (contact_store, book_query); + e_book_query_unref (book_query); + + g_signal_connect (entry, "changed", G_CALLBACK (entry_changed), contact_store); + + gtk_widget_show_all (window); + + return FALSE; +} + +gint +main (gint argc, + gchar **argv) +{ + const gchar *param; + + gtk_init (&argc, &argv); + + if (argc < 2) + param = "???"; + else + param = argv[1]; + + g_idle_add ((GSourceFunc) start_test, (gpointer) param); + + gtk_main (); + + return 0; +} diff --git a/e-util/test-dateedit.c b/e-util/test-dateedit.c new file mode 100644 index 0000000000..5592afbc70 --- /dev/null +++ b/e-util/test-dateedit.c @@ -0,0 +1,299 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Authors: + * Damon Chaplin <damon@ximian.com> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +/* + * test-dateedit - tests the EDateEdit widget. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> +#include "e-dateedit.h" + +static void delete_event_cb (GtkWidget *widget, + GdkEventAny *event, + GtkWidget *window); +static void on_get_date_clicked (GtkWidget *button, + EDateEdit *dedit); +static void on_toggle_24_hour_clicked (GtkWidget *button, + EDateEdit *dedit); +static void on_changed (EDateEdit *dedit, + gchar *name); +#if 0 +static void on_date_changed (EDateEdit *dedit, + gchar *name); +static void on_time_changed (EDateEdit *dedit, + gchar *name); +#endif + +gint +main (gint argc, + gchar **argv) +{ + GtkWidget *window; + EDateEdit *dedit; + GtkWidget *table, *button; + + gtk_init (&argc, &argv); + + puts ("here"); + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title (GTK_WINDOW (window), "EDateEdit Test"); + gtk_window_set_default_size (GTK_WINDOW (window), 300, 200); + gtk_window_set_resizable (GTK_WINDOW (window), TRUE); + gtk_container_set_border_width (GTK_CONTAINER (window), 8); + + g_signal_connect ( + window, "delete_event", + G_CALLBACK (delete_event_cb), window); + + table = gtk_table_new (3, 3, FALSE); + gtk_table_set_row_spacings (GTK_TABLE (table), 4); + gtk_table_set_col_spacings (GTK_TABLE (table), 4); + gtk_widget_show (table); + + gtk_container_add (GTK_CONTAINER (window), table); + + /* EDateEdit 1. */ + dedit = E_DATE_EDIT (e_date_edit_new ()); + gtk_table_attach ( + GTK_TABLE (table), GTK_WIDGET (dedit), + 0, 1, 0, 1, GTK_FILL, GTK_EXPAND, 0, 0); + gtk_widget_show (GTK_WIDGET (dedit)); + +#if 0 + g_signal_connect ( + dedit, "date_changed", + G_CALLBACK (on_date_changed), (gpointer) "1"); + g_signal_connect ( + dedit, "time_changed", + G_CALLBACK (on_time_changed), (gpointer) "1"); +#else + g_signal_connect ( + dedit, "changed", + G_CALLBACK (on_changed), (gpointer) "1"); +#endif + + button = gtk_button_new_with_label ("Print Date"); + gtk_table_attach ( + GTK_TABLE (table), button, + 1, 2, 0, 1, 0, 0, 0, 0); + gtk_widget_show (button); + g_signal_connect ( + button, "clicked", + G_CALLBACK (on_get_date_clicked), dedit); + + /* EDateEdit 2. */ + dedit = E_DATE_EDIT (e_date_edit_new ()); + gtk_table_attach ( + GTK_TABLE (table), (GtkWidget *) dedit, + 0, 1, 1, 2, GTK_FILL, GTK_EXPAND, 0, 0); + gtk_widget_show ((GtkWidget *) (dedit)); + e_date_edit_set_week_start_day (dedit, 1); + e_date_edit_set_show_week_numbers (dedit, TRUE); + e_date_edit_set_use_24_hour_format (dedit, FALSE); + e_date_edit_set_time_popup_range (dedit, 8, 18); + e_date_edit_set_show_time (dedit, FALSE); + +#if 0 + g_signal_connect ( + dedit, "date_changed", + G_CALLBACK (on_date_changed), (gpointer) "2"); + g_signal_connect ( + dedit, "time_changed", + G_CALLBACK (on_time_changed), (gpointer) "2"); +#else + g_signal_connect ( + dedit, "changed", + G_CALLBACK (on_changed), (gpointer) "2"); +#endif + + button = gtk_button_new_with_label ("Print Date"); + gtk_table_attach ( + GTK_TABLE (table), button, + 1, 2, 1, 2, 0, 0, 0, 0); + gtk_widget_show (button); + g_signal_connect ( + button, "clicked", + G_CALLBACK (on_get_date_clicked), dedit); + + /* EDateEdit 3. */ + dedit = E_DATE_EDIT (e_date_edit_new ()); + gtk_table_attach ( + GTK_TABLE (table), (GtkWidget *) dedit, + 0, 1, 2, 3, GTK_FILL, GTK_EXPAND, 0, 0); + gtk_widget_show ((GtkWidget *) (dedit)); + e_date_edit_set_week_start_day (dedit, 1); + e_date_edit_set_show_week_numbers (dedit, TRUE); + e_date_edit_set_use_24_hour_format (dedit, FALSE); + e_date_edit_set_time_popup_range (dedit, 8, 18); + e_date_edit_set_allow_no_date_set (dedit, TRUE); + +#if 0 + g_signal_connect ( + dedit, "date_changed", + G_CALLBACK (on_date_changed), (gpointer) "3"); + g_signal_connect ( + dedit, "time_changed", + G_CALLBACK (on_time_changed), (gpointer) "3"); +#else + g_signal_connect ( + dedit, "changed", + G_CALLBACK (on_changed), (gpointer) "3"); +#endif + + button = gtk_button_new_with_label ("Print Date"); + gtk_table_attach ( + GTK_TABLE (table), button, + 1, 2, 2, 3, 0, 0, 0, 0); + gtk_widget_show (button); + g_signal_connect ( + button, "clicked", + G_CALLBACK (on_get_date_clicked), dedit); + + button = gtk_button_new_with_label ("Toggle 24-hour"); + gtk_table_attach ( + GTK_TABLE (table), button, + 2, 3, 2, 3, 0, 0, 0, 0); + gtk_widget_show (button); + g_signal_connect ( + button, "clicked", + G_CALLBACK (on_toggle_24_hour_clicked), dedit); + + gtk_widget_show (window); + + gtk_main (); + + return 0; +} + +static void +delete_event_cb (GtkWidget *widget, + GdkEventAny *event, + GtkWidget *window) +{ + gtk_widget_destroy (window); + + gtk_main_quit (); +} + +static void +on_get_date_clicked (GtkWidget *button, + EDateEdit *dedit) +{ + time_t t; + + t = e_date_edit_get_time (dedit); + if (t == -1) + g_print ("Time: None\n"); + else + g_print ("Time: %s", ctime (&t)); + + if (!e_date_edit_date_is_valid (dedit)) + g_print (" Date invalid\n"); + + if (!e_date_edit_time_is_valid (dedit)) + g_print (" Time invalid\n"); +} + +static void +on_toggle_24_hour_clicked (GtkWidget *button, + EDateEdit *dedit) +{ + gboolean use_24_hour_format; + + use_24_hour_format = e_date_edit_get_use_24_hour_format (dedit); + e_date_edit_set_use_24_hour_format (dedit, !use_24_hour_format); +} + +#if 0 +static void +on_date_changed (EDateEdit *dedit, + gchar *name) +{ + gint year, month, day; + + if (e_date_edit_date_is_valid (dedit)) { + if (e_date_edit_get_date (dedit, &year, &month, &day)) { + g_print ( + "Date %s changed to: %i/%i/%i (M/D/Y)\n", + name, month, day, year); + } else { + g_print ("Date %s changed to: None\n", name); + } + } else { + g_print ("Date %s changed to: Not Valid\n", name); + } +} + +static void +on_time_changed (EDateEdit *dedit, + gchar *name) +{ + gint hour, minute; + + if (e_date_edit_time_is_valid (dedit)) { + if (e_date_edit_get_time_of_day (dedit, &hour, &minute)) { + g_print ( + "Time %s changed to: %02i:%02i\n", name, + hour, minute); + } else { + g_print ("Time %s changed to: None\n", name); + } + } else { + g_print ("Time %s changed to: Not Valid\n", name); + } +} +#endif + +static void +on_changed (EDateEdit *dedit, + gchar *name) +{ + gint year, month, day, hour, minute; + + g_print ("Date %s changed ", name); + + if (e_date_edit_date_is_valid (dedit)) { + if (e_date_edit_get_date (dedit, &year, &month, &day)) { + g_print ("M/D/Y: %i/%i/%i", month, day, year); + } else { + g_print ("None"); + } + } else { + g_print ("Date Invalid"); + } + + if (e_date_edit_time_is_valid (dedit)) { + if (e_date_edit_get_time_of_day (dedit, &hour, &minute)) { + g_print (" %02i:%02i\n", hour, minute); + } else { + g_print (" None\n"); + } + } else { + g_print (" Time Invalid\n"); + } +} + diff --git a/e-util/test-mail-signatures.c b/e-util/test-mail-signatures.c new file mode 100644 index 0000000000..3dc5f0a720 --- /dev/null +++ b/e-util/test-mail-signatures.c @@ -0,0 +1,195 @@ +/* + * test-mail-signatures.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include <stdlib.h> + +#include <libedataserver/libedataserver.h> + +#include <e-util/e-util.h> + +static GCancellable *cancellable = NULL; + +static void +signature_loaded_cb (EMailSignatureComboBox *combo_box, + GAsyncResult *result, + EWebView *web_view) +{ + gchar *contents = NULL; + gboolean is_html; + GError *error = NULL; + + e_mail_signature_combo_box_load_selected_finish ( + combo_box, result, &contents, NULL, &is_html, &error); + + /* Ignore cancellations. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warn_if_fail (contents == NULL); + g_object_unref (web_view); + g_error_free (error); + return; + + } else if (error != NULL) { + g_warn_if_fail (contents == NULL); + e_alert_submit ( + E_ALERT_SINK (web_view), + "widgets:no-load-signature", + error->message, NULL); + g_object_unref (web_view); + g_error_free (error); + return; + } + + if (contents == NULL) + e_web_view_clear (web_view); + else if (is_html) + e_web_view_load_string (web_view, contents); + else { + gchar *string; + + string = g_markup_printf_escaped ("<pre>%s</pre>", contents); + e_web_view_load_string (web_view, string); + g_free (string); + } + + g_free (contents); + + g_object_unref (web_view); +} + +static void +signature_combo_changed_cb (EMailSignatureComboBox *combo_box, + EWebView *web_view) +{ + if (cancellable != NULL) { + g_cancellable_cancel (cancellable); + g_object_unref (cancellable); + } + + cancellable = g_cancellable_new (); + + e_mail_signature_combo_box_load_selected ( + combo_box, G_PRIORITY_DEFAULT, cancellable, + (GAsyncReadyCallback) signature_loaded_cb, + g_object_ref (web_view)); +} + +gint +main (gint argc, + gchar **argv) +{ + ESourceRegistry *registry; + GtkWidget *container; + GtkWidget *widget; + GtkWidget *vbox; + GtkWidget *identity_combo; + GtkWidget *signature_combo; + GError *error = NULL; + + gtk_init (&argc, &argv); + + registry = e_source_registry_new_sync (NULL, &error); + + if (error != NULL) { + g_printerr ("%s\n", error->message); + exit (EXIT_FAILURE); + } + + /* Construct the widgets. */ + + widget = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title (GTK_WINDOW (widget), "Mail Signatures"); + gtk_window_set_default_size (GTK_WINDOW (widget), 400, 400); + gtk_container_set_border_width (GTK_CONTAINER (widget), 12); + gtk_widget_show (widget); + + g_signal_connect ( + widget, "delete-event", + G_CALLBACK (gtk_main_quit), NULL); + + container = widget; + + widget = gtk_vbox_new (FALSE, 12); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + container = vbox = widget; + + widget = gtk_label_new ("<b>EMailSignatureComboBox</b>"); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_vbox_new (FALSE, 6); + gtk_widget_set_margin_left (widget, 12); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = e_mail_signature_combo_box_new (registry); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + signature_combo = widget; + gtk_widget_show (widget); + + widget = e_mail_identity_combo_box_new (registry); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + identity_combo = widget; + gtk_widget_show (widget); + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = e_web_view_new (); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + g_signal_connect ( + signature_combo, "changed", + G_CALLBACK (signature_combo_changed_cb), widget); + + container = vbox; + + widget = gtk_label_new ("<b>EMailSignatureManager</b>"); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = e_mail_signature_manager_new (registry); + gtk_widget_set_margin_left (widget, 12); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + g_object_bind_property ( + identity_combo, "active-id", + signature_combo, "identity-uid", + G_BINDING_SYNC_CREATE); + + gtk_main (); + + return 0; +} diff --git a/e-util/test-name-selector.c b/e-util/test-name-selector.c new file mode 100644 index 0000000000..3744ad9f1a --- /dev/null +++ b/e-util/test-name-selector.c @@ -0,0 +1,102 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* test-name-selector.c - Test for name selector components. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Hans Petter Jansson <hpj@novell.com> + */ + +#include <camel/camel.h> +#include <e-util/e-util.h> + +static ENameSelectorDialog *name_selector_dialog; +static GtkWidget *name_selector_entry_window; + +static void +close_dialog (GtkWidget *widget, + gint response, + gpointer data) +{ + gtk_widget_destroy (GTK_WIDGET (name_selector_dialog)); + gtk_widget_destroy (name_selector_entry_window); + + g_timeout_add (4000, (GSourceFunc) gtk_main_quit, NULL); +} + +static gboolean +start_test (ESourceRegistry *registry) +{ + ENameSelectorModel *name_selector_model; + ENameSelectorEntry *name_selector_entry; + EDestinationStore *destination_store; + GtkWidget *container; + + destination_store = e_destination_store_new (); + name_selector_model = e_name_selector_model_new (); + + e_name_selector_model_add_section (name_selector_model, "to", "To", destination_store); + e_name_selector_model_add_section (name_selector_model, "cc", "Cc", NULL); + e_name_selector_model_add_section (name_selector_model, "bcc", "Bcc", NULL); + + name_selector_dialog = e_name_selector_dialog_new (registry); + e_name_selector_dialog_set_model (name_selector_dialog, name_selector_model); + gtk_window_set_modal (GTK_WINDOW (name_selector_dialog), FALSE); + + name_selector_entry = e_name_selector_entry_new (registry); + e_name_selector_entry_set_destination_store (name_selector_entry, destination_store); + + g_signal_connect (name_selector_dialog, "response", G_CALLBACK (close_dialog), name_selector_dialog); + gtk_widget_show (GTK_WIDGET (name_selector_dialog)); + + container = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_container_add (GTK_CONTAINER (container), GTK_WIDGET (name_selector_entry)); + gtk_widget_show_all (container); + + name_selector_entry_window = container; + + g_object_unref (name_selector_model); + g_object_unref (destination_store); + return FALSE; +} + +gint +main (gint argc, + gchar **argv) +{ + ESourceRegistry *registry; + GError *error = NULL; + + gtk_init (&argc, &argv); + + camel_init (NULL, 0); + + registry = e_source_registry_new_sync (NULL, &error); + + if (error != NULL) { + g_error ( + "Failed to load ESource registry: %s", + error->message); + g_assert_not_reached (); + } + + g_idle_add ((GSourceFunc) start_test, registry); + + gtk_main (); + + return 0; +} diff --git a/e-util/test-preferences-window.c b/e-util/test-preferences-window.c new file mode 100644 index 0000000000..4ad30e2245 --- /dev/null +++ b/e-util/test-preferences-window.c @@ -0,0 +1,108 @@ +/* + * test-preferences-window.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#include "e-preferences-window.c" + +#include <gtk/gtk.h> + +static GtkWidget * +create_page_number (gint i) +{ + gchar *caption; + GtkWidget *widget; + + caption = g_strdup_printf ("Title of page %d", i); + + widget = gtk_label_new (caption); + gtk_widget_show (widget); + + g_free (caption); + + return widget; +} + +static GtkWidget * +create_page_zero (EPreferencesWindow *preferences_window) +{ + return create_page_number (0); +} +static GtkWidget * +create_page_one (EPreferencesWindow *preferences_window) +{ + return create_page_number (1); +} +static GtkWidget * +create_page_two (EPreferencesWindow *preferences_window) +{ + return create_page_number (2); +} + +static void +add_pages (EPreferencesWindow *preferences_window) +{ + e_preferences_window_add_page ( + preferences_window, "page-0", + "gtk-properties", "title 0", NULL, + create_page_zero, 0); + e_preferences_window_add_page ( + preferences_window, "page-1", + "gtk-properties", "title 1", NULL, + create_page_one, 1); + e_preferences_window_add_page ( + preferences_window, "page-2", + "gtk-properties", "title 2", NULL, + create_page_two, 2); +} + +static gint +delete_event_callback (GtkWidget *widget, + GdkEventAny *event, + gpointer data) +{ + gtk_main_quit (); + + return TRUE; +} + +gint +main (gint argc, + gchar **argv) +{ + GtkWidget *window; + + gtk_init (&argc, &argv); + + window = e_preferences_window_new (NULL); + gtk_window_set_default_size (GTK_WINDOW (window), 400, 300); + + g_signal_connect ( + window, "delete-event", + G_CALLBACK (delete_event_callback), NULL); + + add_pages (E_PREFERENCES_WINDOW (window)); + e_preferences_window_setup (E_PREFERENCES_WINDOW (window)); + + gtk_widget_show (window); + + gtk_main (); + + return 0; +} diff --git a/e-util/test-source-combo-box.c b/e-util/test-source-combo-box.c new file mode 100644 index 0000000000..cb40f6eb18 --- /dev/null +++ b/e-util/test-source-combo-box.c @@ -0,0 +1,107 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* test-source-combo-box.c - Test for ESourceComboBox. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Ettore Perazzoli <ettore@ximian.com> + */ + +#include <config.h> +#include <gtk/gtk.h> + +#include <e-util/e-util.h> + +static const gchar *extension_name; + +static void +source_changed_cb (ESourceComboBox *combo_box) +{ + ESource *source; + + source = e_source_combo_box_ref_active (combo_box); + if (source != NULL) { + const gchar *display_name; + display_name = e_source_get_display_name (source); + g_print ("source selected: \"%s\"\n", display_name); + g_object_unref (source); + } else { + g_print ("source selected: (none)\n"); + } +} + +static gint +on_idle_create_widget (ESourceRegistry *registry) +{ + GtkWidget *window; + GtkWidget *box; + GtkWidget *combo_box; + GtkWidget *button; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_container_add (GTK_CONTAINER (window), box); + + combo_box = e_source_combo_box_new (registry, extension_name); + g_signal_connect ( + combo_box, "changed", + G_CALLBACK (source_changed_cb), NULL); + gtk_box_pack_start (GTK_BOX (box), combo_box, FALSE, FALSE, 0); + + button = gtk_toggle_button_new_with_label ("Show Colors"); + gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0); + + g_object_bind_property ( + combo_box, "show-colors", + button, "active", + G_BINDING_SYNC_CREATE | + G_BINDING_BIDIRECTIONAL); + + gtk_widget_show_all (window); + + return FALSE; +} + +gint +main (gint argc, + gchar **argv) +{ + ESourceRegistry *registry; + GError *error = NULL; + + gtk_init (&argc, &argv); + + if (argc < 2) + extension_name = E_SOURCE_EXTENSION_ADDRESS_BOOK; + else + extension_name = argv[1]; + + registry = e_source_registry_new_sync (NULL, &error); + + if (error != NULL) { + g_error ( + "Failed to load ESource registry: %s", + error->message); + g_assert_not_reached (); + } + + g_idle_add ((GSourceFunc) on_idle_create_widget, registry); + + gtk_main (); + + return 0; +} diff --git a/e-util/test-source-config.c b/e-util/test-source-config.c new file mode 100644 index 0000000000..4a5ce30d91 --- /dev/null +++ b/e-util/test-source-config.c @@ -0,0 +1,57 @@ +#include <stdlib.h> +#include <gtk/gtk.h> + +#include <libedataserver/libedataserver.h> + +#include "e-source-config-dialog.h" + +static void +dialog_response (GtkDialog *dialog, + gint response_id) +{ + gtk_main_quit (); +} + +gint +main (gint argc, + gchar **argv) +{ + ESourceRegistry *registry; + ESource *source = NULL; + GtkWidget *config; + GtkWidget *dialog; + GError *error = NULL; + + gtk_init (&argc, &argv); + + registry = e_source_registry_new_sync (NULL, &error); + + if (error != NULL) { + g_printerr ("%s\n", error->message); + exit (EXIT_FAILURE); + } + + if (argc > 1) { + source = e_source_registry_ref_source (registry, argv[1]); + if (source == NULL) { + g_printerr ("No such UID: %s\n", argv[1]); + exit (EXIT_FAILURE); + } + } + + config = e_source_config_new (registry, source); + dialog = e_source_config_dialog_new (E_SOURCE_CONFIG (config)); + + g_signal_connect ( + dialog, "response", + G_CALLBACK (dialog_response), NULL); + + gtk_widget_show (config); + gtk_widget_show (dialog); + + g_object_unref (source); + + gtk_main (); + + return 0; +} diff --git a/e-util/test-source-selector.c b/e-util/test-source-selector.c new file mode 100644 index 0000000000..0c1a77289e --- /dev/null +++ b/e-util/test-source-selector.c @@ -0,0 +1,157 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* test-source-list-selector.c - Test program for the ESourceListSelector + * widget. + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Ettore Perazzoli <ettore@ximian.com> + */ + +#include <e-util/e-util.h> + +static const gchar *extension_name; + +static void +dump_selection (ESourceSelector *selector) +{ + GSList *selection = e_source_selector_get_selection (selector); + + g_print ("Current selection:\n"); + if (selection == NULL) { + g_print ("\t(None)\n"); + } else { + GSList *p; + + for (p = selection; p != NULL; p = p->next) { + ESource *source = E_SOURCE (p->data); + ESourceBackend *extension; + + extension = e_source_get_extension ( + source, extension_name); + + g_print ( + "\tSource %s (backend %s)\n", + e_source_get_display_name (source), + e_source_backend_get_backend_name (extension)); + } + } + + e_source_selector_free_selection (selection); +} + +static void +selection_changed_callback (ESourceSelector *selector) +{ + g_print ("Selection changed!\n"); + dump_selection (selector); +} + +static gint +on_idle_create_widget (ESourceRegistry *registry) +{ + GtkWidget *window; + GtkWidget *vgrid; + GtkWidget *selector; + GtkWidget *scrolled_window; + GtkWidget *check; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_default_size (GTK_WINDOW (window), 200, 300); + + g_signal_connect ( + window, "delete-event", + G_CALLBACK (gtk_main_quit), NULL); + + vgrid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_VERTICAL, + "column-homogeneous", FALSE, + "row-spacing", 6, + NULL); + gtk_container_add (GTK_CONTAINER (window), vgrid); + + selector = e_source_selector_new (registry, extension_name); + g_signal_connect ( + selector, "selection_changed", + G_CALLBACK (selection_changed_callback), NULL); + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (scrolled_window), GTK_SHADOW_IN); + gtk_container_add (GTK_CONTAINER (scrolled_window), selector); + gtk_widget_set_hexpand (scrolled_window, TRUE); + gtk_widget_set_halign (scrolled_window, GTK_ALIGN_FILL); + gtk_widget_set_vexpand (scrolled_window, TRUE); + gtk_widget_set_valign (scrolled_window, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (vgrid), scrolled_window); + + check = gtk_check_button_new_with_label ("Show colors"); + gtk_widget_set_halign (check, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (vgrid), check); + + g_object_bind_property ( + selector, "show-colors", + check, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + check = gtk_check_button_new_with_label ("Show toggles"); + gtk_widget_set_halign (check, GTK_ALIGN_FILL); + gtk_container_add (GTK_CONTAINER (vgrid), check); + + g_object_bind_property ( + selector, "show-toggles", + check, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + gtk_widget_show_all (window); + + return FALSE; +} + +gint +main (gint argc, + gchar **argv) +{ + ESourceRegistry *registry; + GError *error = NULL; + + gtk_init (&argc, &argv); + + if (argc < 2) + extension_name = E_SOURCE_EXTENSION_ADDRESS_BOOK; + else + extension_name = argv[1]; + + registry = e_source_registry_new_sync (NULL, &error); + + if (error != NULL) { + g_error ( + "Failed to load ESource registry: %s", + error->message); + g_assert_not_reached (); + } + + g_idle_add ((GSourceFunc) on_idle_create_widget, registry); + + gtk_main (); + + return 0; +} diff --git a/e-util/tree-expanded.xpm b/e-util/tree-expanded.xpm new file mode 100644 index 0000000000..94d162d40b --- /dev/null +++ b/e-util/tree-expanded.xpm @@ -0,0 +1,23 @@ +/* XPM */ +static const gchar *tree_expanded_xpm[] = { +"16 16 4 1", +" c None", +". c #FFFFFF", +"* c #000000", +"+ c #666666", +" ", +" ", +" ", +" ", +" +++++++++ ", +" +.......+ ", +" +.......+ ", +" +.......+ ", +" +.*****.+ ", +" +.......+ ", +" +.......+ ", +" +.......+ ", +" +++++++++ ", +" ", +" ", +" "}; diff --git a/e-util/tree-unexpanded.xpm b/e-util/tree-unexpanded.xpm new file mode 100644 index 0000000000..d20ec5aa33 --- /dev/null +++ b/e-util/tree-unexpanded.xpm @@ -0,0 +1,23 @@ +/* XPM */ +static const gchar *tree_unexpanded_xpm[] = { +"16 16 4 1", +" c None", +". c #FFFFFF", +"* c #000000", +"+ c #666666", +" ", +" ", +" ", +" ", +" +++++++++ ", +" +.......+ ", +" +...*...+ ", +" +...*...+ ", +" +.*****.+ ", +" +...*...+ ", +" +...*...+ ", +" +.......+ ", +" +++++++++ ", +" ", +" ", +" "}; diff --git a/e-util/widgets.error.xml b/e-util/widgets.error.xml new file mode 100644 index 0000000000..efaa41c42e --- /dev/null +++ b/e-util/widgets.error.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<error-list domain="widgets"> + + <error id="ask-signature-changed" type="question" default="GTK_RESPONSE_YES"> + <_primary>Do you wish to save your changes?</_primary> + <_secondary xml:space="preserve">This signature has been changed, but has not been saved.</_secondary> + <button _label="_Discard changes" response="GTK_RESPONSE_NO"/> + <button stock="gtk-cancel" response="GTK_RESPONSE_CANCEL"/> + <button stock="gtk-save" response="GTK_RESPONSE_YES"/> + </error> + + <error id="blank-signature" type="error"> + <_primary>Blank Signature</_primary> + <_secondary>Please provide an unique name to identify this signature.</_secondary> + </error> + + <error id="no-load-signature" type="error"> + <_primary>Could not load signature.</_primary> + <secondary xml:space="preserve">{0}</secondary> + </error> + + <error id="no-save-signature" type="error"> + <_primary>Could not save signature.</_primary> + <secondary xml:space="preserve">{0}</secondary> + </error> + +</error-list> + |