Added a new dialog to generate CTABLES
authorJohn Darrington <john@cellform.com>
Tue, 20 Jun 2023 16:42:24 +0000 (18:42 +0200)
committerJohn Darrington <john@cellform.com>
Tue, 20 Jun 2023 16:42:24 +0000 (18:42 +0200)
This dialog provides just a small subset of what ought to be available. In
particular, nested tables cannot be specified and only the default summary
functions work.  Other caveats are also present, but it generally provides
a start and proof of concept.

src/ui/gui/automake.mk
src/ui/gui/ctables.ui [new file with mode: 0644]
src/ui/gui/data-editor.ui
src/ui/gui/dummy.c
src/ui/gui/psppire-data-window.c
src/ui/gui/psppire-dialog-action-ctables.c [new file with mode: 0644]
src/ui/gui/psppire-dialog-action-ctables.h [new file with mode: 0644]
src/ui/gui/widgets.c

index e01090777c4e7df05a7dae4f492494888d5042ef..6b8c4bf81da7cd0ad3b64a4e2d60104344365d55 100644 (file)
@@ -27,6 +27,7 @@ UI_FILES = \
        src/ui/gui/comments.ui \
        src/ui/gui/crosstabs.ui \
        src/ui/gui/chi-square.ui \
+       src/ui/gui/ctables.ui \
        src/ui/gui/descriptives.ui \
        src/ui/gui/entry-dialog.ui \
        src/ui/gui/examine.ui \
@@ -317,6 +318,8 @@ src_ui_gui_libwidgets_essential_la_SOURCES = \
        src/ui/gui/psppire-dialog-action-sort.h \
        src/ui/gui/psppire-dialog-action-split.c \
        src/ui/gui/psppire-dialog-action-split.h \
+       src/ui/gui/psppire-dialog-action-ctables.c \
+       src/ui/gui/psppire-dialog-action-ctables.h \
        src/ui/gui/psppire-dialog-action-tt1s.c \
        src/ui/gui/psppire-dialog-action-tt1s.h \
        src/ui/gui/psppire-dialog-action-two-sample.c \
diff --git a/src/ui/gui/ctables.ui b/src/ui/gui/ctables.ui
new file mode 100644 (file)
index 0000000..14dc602
--- /dev/null
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 -->
+<!-- PSPP - a program for statistical analysis. -->
+<!-- Copyright (C) 2023 Free Software Foundation, Inc. -->
+<!-- This program is free software: you can redistribute it and/or modify -->
+<!-- it under the terms of the GNU General Public License as published by -->
+<!-- the Free Software Foundation, either version 3 of the License, or -->
+<!-- (at your option) any later version. -->
+<!-- This program is distributed in the hope that it will be useful, -->
+<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
+<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -->
+<!-- GNU General Public License for more details. -->
+<!-- You should have received a copy of the GNU General Public License -->
+<!-- along with this program.  If not, see <http://www.gnu.org/licenses/>. -->
+<interface>
+  <requires lib="gtk+" version="3.22"/>
+  <requires lib="psppire" version="2054.17080"/>
+  <object class="PsppireDialog" id="tables-dialog">
+    <property name="can-focus">False</property>
+    <property name="title" translatable="yes">Custiom Tables</property>
+    <property name="modal">True</property>
+    <property name="help-page">CTABLES</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkPaned">
+            <property name="visible">True</property>
+            <property name="can-focus">True</property>
+            <property name="position">200</property>
+            <property name="position-set">True</property>
+            <property name="wide-handle">True</property>
+            <child>
+              <object class="GtkScrolledWindow" id="variables">
+                <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="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="hscrollbar-policy">never</property>
+                <property name="shadow-type">etched-in</property>
+                <child>
+                  <object class="PsppireDictView" id="dict-view">
+                    <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="border-width">0</property>
+                    <property name="headers-visible">False</property>
+                    <property name="selection-mode">single</property>
+                    <child internal-child="selection">
+                      <object class="GtkTreeSelection"/>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="resize">False</property>
+                <property name="shrink">True</property>
+              </packing>
+            </child>
+            <child>
+              <!-- n-columns=2 n-rows=2 -->
+              <object class="GtkGrid">
+                <property name="visible">True</property>
+                <property name="can-focus">False</property>
+                <property name="has-tooltip">True</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <child>
+                  <object class="GtkDrawingArea">
+                    <property name="width-request">30</property>
+                    <property name="height-request">30</property>
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="opacity">0</property>
+                  </object>
+                  <packing>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkDrawingArea" id="columns-pad">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="hexpand">True</property>
+                  </object>
+                  <packing>
+                    <property name="left-attach">1</property>
+                    <property name="top-attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkDrawingArea" id="rows-pad">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="vexpand">True</property>
+                  </object>
+                  <packing>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkDrawingArea" id="template-canvas">
+                    <property name="name">fred</property>
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="has-tooltip">True</property>
+                  </object>
+                  <packing>
+                    <property name="left-attach">1</property>
+                    <property name="top-attach">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="resize">True</property>
+                <property name="shrink">True</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="PsppireButtonBox" id="psppire-vbuttonbox1">
+            <property name="visible">True</property>
+            <property name="can-focus">False</property>
+            <property name="border-width">5</property>
+            <property name="buttons">PSPPIRE_BUTTON_OK_MASK | PSPPIRE_BUTTON_CANCEL_MASK | PSPPIRE_BUTTON_HELP_MASK | PSPPIRE_BUTTON_RESET_MASK | PSPPIRE_BUTTON_PASTE_MASK</property>
+            <property name="default">PSPPIRE_BUTTON_OK_MASK</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="pack-type">end</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
index 214af79738740740d0cdcbce05bbcc78df811a8a..b7f2456af9cc00f764dc29e0032370eca5f6e547 100644 (file)
            <attribute name="action">win.PsppireDialogActionCrosstabs</attribute>
          </item>
        </submenu>
+       <item>
+         <attribute name="label" translatable="yes">_Tables...</attribute>
+         <attribute name="action">win.PsppireDialogActionCtables</attribute>
+       </item>
        <submenu>
          <attribute name="label" translatable="yes">Compare _Means</attribute>
          <item>
index 50e0d6529a940a97f067399f653e76b38219f4a4..93090636b6f918984e1c3de8efb66c714fe4e63b 100644 (file)
@@ -35,6 +35,7 @@
 #include "t-test-options.h"
 #include "src/language/commands/chart-category.h"
 #include "src/language/commands/aggregate.h"
+#include "libpspp/llx.h"
 
 const GEnumValue align[1];
 const GEnumValue measure[1];
@@ -75,3 +76,5 @@ psppire_data_store_string_to_value (GtkTreeModel *model, gint col, gint row,
   assert (0);
   return FALSE;
 }
+
+const struct llx_manager llx_malloc_mgr =  {NULL, NULL, NULL};
index ecb9f121af44d08d2a14f246e99097c354b0a903..d581f57ebbd1075ca754cdb341f184481d7edc25 100644 (file)
@@ -1,6 +1,6 @@
 /* PSPPIRE - a graphical user interface for PSPP.
    Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013, 2014,
-   2016, 2017  Free Software Foundation
+   2016, 2017, 2023  Free Software Foundation
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -87,6 +87,7 @@
 #include "psppire-dialog-action-select.h"
 #include "psppire-dialog-action-sort.h"
 #include "psppire-dialog-action-split.h"
+#include "psppire-dialog-action-ctables.h"
 #include "psppire-dialog-action-tt1s.h"
 #include "psppire-dialog-action-two-sample.h"
 #include "psppire-dialog-action-univariate.h"
@@ -1049,7 +1050,7 @@ on_cut (PsppireDataWindow *dw)
          sel.start_y = sel.end_y;
          sel.end_y = tmp;
        }
-         
+
       GtkClipboard *clip =
        gtk_clipboard_get_for_display (gtk_widget_get_display (GTK_WIDGET (dw)),
                                       GDK_SELECTION_CLIPBOARD);
@@ -1538,6 +1539,7 @@ psppire_data_window_finish_init (PsppireDataWindow *de,
   connect_dialog_action (PSPPIRE_TYPE_DIALOG_ACTION_BINOMIAL, de);
   connect_dialog_action (PSPPIRE_TYPE_DIALOG_ACTION_RUNS, de);
   connect_dialog_action (PSPPIRE_TYPE_DIALOG_ACTION_1SKS, de);
+  connect_dialog_action (PSPPIRE_TYPE_DIALOG_ACTION_CTABLES, de);
   connect_dialog_action (PSPPIRE_TYPE_DIALOG_ACTION_TWO_SAMPLE, de);
   connect_dialog_action (PSPPIRE_TYPE_DIALOG_ACTION_K_RELATED, de);
   connect_dialog_action (PSPPIRE_TYPE_DIALOG_ACTION_K_INDEPENDENT, de);
diff --git a/src/ui/gui/psppire-dialog-action-ctables.c b/src/ui/gui/psppire-dialog-action-ctables.c
new file mode 100644 (file)
index 0000000..f59c2b1
--- /dev/null
@@ -0,0 +1,585 @@
+/* PSPPIRE - a graphical user interface for PSPP.
+   Copyright (C) 2023  Free Software Foundation
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+
+#include <config.h>
+
+#include "psppire-dialog-action-ctables.h"
+#include "psppire-value-entry.h"
+
+#include "dialog-common.h"
+#include <ui/syntax-gen.h>
+#include "psppire-var-view.h"
+
+#include "psppire-dialog.h"
+#include "builder-wrapper.h"
+
+#include "psppire-dict.h"
+#include "libpspp/str.h"
+#include "libpspp/llx.h"
+
+#include "psppire-dictview.h"
+
+#include "output/cairo-fsm.h"
+#include "output/output-item.h"
+#include "output/pivot-table.h"
+#include "data/value-labels.h"
+
+#include <gettext.h>
+#define _(msgid) gettext (msgid)
+#define N_(msgid) msgid
+
+static struct xr_fsm_style *get_xr_fsm_style (GtkWidget *w);
+static void psppire_dialog_action_ctables_class_init
+     (PsppireDialogActionCtablesClass *class);
+
+G_DEFINE_TYPE (PsppireDialogActionCtables, psppire_dialog_action_ctables,
+               PSPPIRE_TYPE_DIALOG_ACTION);
+
+
+/* Create the basis of a table.  This table contasins just two dimensions
+   and nothing else. */
+static struct pivot_table *make_table (void)
+{
+  struct pivot_table *table = pivot_table_create ("$ctables-dialog-template");
+  table->show_title = false;
+  table->show_caption = false;
+
+  pivot_dimension_create (table, PIVOT_AXIS_ROW, "row");
+  pivot_dimension_create (table, PIVOT_AXIS_COLUMN, "column");
+
+  return table;
+}
+
+/* Create a new text leaf in CAT with the string TEXT iff there isn't already
+   such a leaf */
+static int
+category_create_leaf_once (struct pivot_category *cat, const char *text)
+{
+  for (int s = 0; s < cat->n_subs; ++s)
+    {
+      if (cat->subs[s]->name->type == PIVOT_VALUE_TEXT)
+        {
+        if (0 == strcmp (cat->subs[s]->name->text.id, text))
+          return -1;
+        }
+      else
+        return -1;
+    }
+
+  return pivot_category_create_leaf (cat, pivot_value_new_text (text));
+}
+
+
+/* Add a new pivot category to PARENT.
+
+   CHILDREN must be NULL or a list of pivot_values.  CHILD_NAME is the name of
+   the new category.
+
+   If CHILDREN is NULL or a empty, then the new category will be a leaf with
+   the name CHILD_NAME.  Otherwise the new category will be a group and the
+   contents of CHILDREN will be the leaves of that group.
+ */
+static void
+add_child_category (struct pivot_category *parent, const char *child_name,
+                   struct llx_list *children)
+{
+  if (children && llx_is_empty (children))
+    {
+      pivot_category_create_leaf (parent, pivot_value_new_text (child_name));
+      return;
+    }
+
+  for (struct llx *llx = llx_head (children); llx != llx_null (children);
+       llx = llx_next (llx))
+    {
+      struct pivot_value *foo = llx_data (llx);
+      struct pivot_category *pc = pivot_category_create_group (parent, child_name);
+      pivot_category_create_leaf (pc, foo);
+    }
+}
+
+
+/*
+  Supplement TABLE with a category to hold cells which could contain summary
+  data for VAR.   PRIMARY_AXIS is the TABLE's axis which will contain the
+  heading for the variable itself.  The perpendicular axis will contain the
+  headings of the summary functions.
+
+  DICT is the dictionary which contains VAR and all previously added variables.
+
+  Returns TRUE if successfull.  False otherwise.
+ */
+static gboolean
+augment_template_table (struct pivot_table *table,
+                        enum pivot_axis_type primary_axis,
+                        const struct variable *var, const struct dictionary *dict)
+{
+  g_return_val_if_fail (table, FALSE);
+  struct pivot_dimension *axis0 ;
+  struct pivot_dimension *axis1 ;
+
+  g_assert (primary_axis == PIVOT_AXIS_ROW || primary_axis == PIVOT_AXIS_COLUMN);
+
+  if (primary_axis == PIVOT_AXIS_ROW)
+    {
+      axis0 = table->dimensions[0];
+      axis1 = table->dimensions[1];
+    }
+  else
+    {
+      axis0 = table->dimensions[1];
+      axis1 = table->dimensions[0];
+    }
+
+  const enum measure m = var_get_measure (var);
+  struct pivot_value *pv_var = pivot_value_new_variable (var);
+
+  /* Displaying the variable label in the template tends to make it too verbose
+     and hard to read.  So we remove the label here. */
+  free (pv_var->variable.var_label);
+  pv_var->variable.var_label = NULL;
+
+  if (m == MEASURE_NOMINAL || m == MEASURE_ORDINAL)
+    {
+      /* If this axis already contains headings for summary functions,
+         these need to be transferred to a sub category below the one
+         that we are adding.   So make a list of them here.  */
+      struct llx_list summary_categories;
+      llx_init (&summary_categories);
+      for (int s = 0; s < axis0->root->n_subs; ++s)
+        {
+          if (axis0->root->subs[s]->name->type == PIVOT_VALUE_TEXT)
+            {
+              struct pivot_value *subtext
+                = pivot_value_clone (axis0->root->subs[s]->name);
+              llx_push_tail (&summary_categories, subtext, &llx_malloc_mgr);
+            }
+        }
+      struct pivot_category *cat =
+        pivot_category_create_group__ (axis0->root, pv_var);
+
+      /* The value labels (if any) form the categories */
+      if (var_has_value_labels (var))
+        {
+          const struct val_labs *labels = var_get_value_labels (var);
+          size_t count = val_labs_count (labels);
+
+          const struct val_lab **array = val_labs_sorted (labels);
+          for (int i = 0; i < count; ++i)
+            {
+              add_child_category (cat, array[i]->label, &summary_categories);
+            }
+          free (array);
+        }
+      else
+        {
+          add_child_category (cat, N_("Category 0"), &summary_categories);
+          add_child_category (cat, N_("Category 1"), &summary_categories);
+        }
+      category_create_leaf_once (axis1->root, N_("Count"));
+
+      llx_destroy (&summary_categories, NULL, NULL, &llx_malloc_mgr);
+    }
+  else
+    {
+      /* When adding a scalar variable we must check that the other axis
+         doesn't also contain scalar variables.  This is not allowed.  */
+      for (int s = 0; s < axis1->root->n_subs; ++s)
+        {
+          const struct pivot_value *name = axis1->root->subs[s]->name;
+          if (name->type == PIVOT_VALUE_VARIABLE)
+            {
+              const struct variable *v
+                = dict_lookup_var (dict, name->variable.var_name);
+              g_return_val_if_fail (v, FALSE);
+              enum measure meas = var_get_measure (v);
+              if (meas != MEASURE_NOMINAL && meas != MEASURE_ORDINAL)
+                {
+                  return FALSE;
+                }
+            }
+        }
+      pivot_category_create_leaf (axis0->root, pv_var);
+      category_create_leaf_once (axis1->root, N_("Mean"));
+    }
+
+  return  TRUE;
+}
+
+static gboolean
+dialog_state_valid (PsppireDialogAction *pda)
+{
+  PsppireDialogActionCtables *act = PSPPIRE_DIALOG_ACTION_CTABLES (pda);
+
+  if (!act->table)
+    return FALSE;
+
+  if (act->table->n_dimensions < 2)
+    return FALSE;
+
+  for (int d = 0; d < act->table->n_dimensions; ++d)
+    {
+      if (act->table->dimensions[d]->root->n_subs <= 0)
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+static void
+refresh (PsppireDialogAction *pda)
+{
+  PsppireDialogActionCtables *act = PSPPIRE_DIALOG_ACTION_CTABLES (pda);
+
+  output_item_unref (act->graphic);
+  act->graphic = NULL;
+
+  act->table = make_table ();
+  act->table = pivot_table_ref (act->table);
+
+  gtk_widget_queue_draw (act->canvas);
+  act->dragged_variable = NULL;
+}
+
+static gboolean
+pad_draw (GtkWidget *widget, cairo_t *cr, gpointer data)
+{
+  GdkRGBA color;
+  GtkStyleContext *context = gtk_widget_get_style_context (widget);
+
+  guint width = gtk_widget_get_allocated_width (widget);
+  guint height = gtk_widget_get_allocated_height (widget);
+
+  gtk_render_background (context, cr, 0, 0, width, height);
+
+  cairo_rectangle (cr, 2, 2, width - 5, height - 5);
+
+  gtk_style_context_get_color (context,
+                               GTK_STATE_FLAG_DROP_ACTIVE,
+                               &color);
+
+  gdk_cairo_set_source_rgba (cr, &color);
+
+  const double dashes[] = {10.0, 2.0};
+  cairo_set_dash (cr, dashes, 2, 0.0);
+  cairo_stroke (cr);
+
+  cairo_rectangle (cr, 3, 3, width - 7, height - 7);
+  color.red *= 0.5;
+  color.green *= 0.5;
+  color.blue *= 0.5;
+  color.alpha *= 0.25;
+  gdk_cairo_set_source_rgba (cr, &color);
+  cairo_fill (cr);
+
+ return FALSE;
+}
+
+static void
+drag_begin (PsppireDictView  *widget,
+            GdkDragContext  *context,
+            PsppireDialogActionCtables *act)
+{
+  act->dragged_variable =
+    psppire_dict_view_get_selected_variable (widget);
+
+  if (!act->dragged_variable)
+    {
+      gtk_drag_cancel (context);
+      return;
+    }
+
+  /* Set the icon to be displayed during dragging operation */
+  enum measure m = var_get_measure (act->dragged_variable);
+  struct fmt_spec fmt =   var_get_print_format (act->dragged_variable);
+  const char *stock_id = get_var_measurement_stock_id (fmt.type, m);
+
+  gtk_drag_set_icon_name (context, stock_id, 0, 0);
+}
+
+static void
+drag_end (PsppireDictView  *widget,
+          GdkDragContext  *context,
+          PsppireDialogActionCtables *act)
+{
+  act->dragged_variable = NULL;
+}
+
+static gboolean
+drag_failed (GtkWidget      *widget,
+             GdkDragContext *context,
+             GtkDragResult   result,
+             PsppireDialogActionCtables *act)
+{
+  act->dragged_variable = NULL;
+  return FALSE;
+}
+
+static gboolean
+drag_drop_pad (GtkWidget      *widget,
+               GdkDragContext *context,
+               int             x,
+               int             y,
+               guint           time,
+               PsppireDialogAction *pda)
+{
+  PsppireDialogActionCtables *act = PSPPIRE_DIALOG_ACTION_CTABLES (pda);
+
+  enum pivot_axis_type axis
+    = (act->rows_pad == widget) ? PIVOT_AXIS_ROW : PIVOT_AXIS_COLUMN;
+
+  PsppireDict *dict = PSPPIRE_DICT_VIEW (pda->source)->dict;
+
+  gboolean ok
+    = augment_template_table (act->table, axis, act->dragged_variable,
+                              dict->dict);
+  gtk_drag_finish (context, ok, FALSE, time);
+
+  if (!ok)
+    return TRUE;
+
+  act->table = pivot_table_ref (act->table);
+
+  output_item_unref (act->graphic);
+  act->graphic = table_item_create (pivot_table_unshare (act->table));
+
+  gtk_widget_queue_draw (act->canvas);
+
+  return TRUE;
+}
+
+static gchar f1[]="ctables-dialog";
+
+static const GtkTargetEntry te[1] = {
+    {f1, GTK_TARGET_SAME_APP, 2},
+  };
+
+static gboolean
+canvas_draw (GtkWidget *widget, cairo_t *cr, PsppireDialogActionCtables *act)
+{
+  GdkRectangle clip;
+  if (!gdk_cairo_get_clip_rectangle (cr, &clip))
+    return TRUE;
+  struct xr_fsm_style *style = NULL;
+  struct xr_fsm *fsm = NULL;
+
+
+  GdkRGBA color;
+  GtkStyleContext *context = gtk_widget_get_style_context (widget);
+
+  guint width = gtk_widget_get_allocated_width (widget);
+  guint height = gtk_widget_get_allocated_height (widget);
+
+  gtk_render_background (context, cr, 0, 0, width, height);
+
+  if (act->graphic)
+    {
+      style = get_xr_fsm_style (widget);
+      fsm = xr_fsm_create_for_scrolling (act->graphic, style, cr);
+      xr_fsm_draw_region (fsm, cr, clip.x, clip.y, clip.width, clip.height);
+    }
+
+  gtk_style_context_get_color (context,
+                               gtk_style_context_get_state (context),
+                               &color);
+  gdk_cairo_set_source_rgba (cr, &color);
+
+  cairo_fill (cr);
+
+  if (fsm)
+    xr_fsm_destroy (fsm);
+
+  if (style)
+      xr_fsm_style_unref (style);
+
+  return FALSE;
+}
+
+static struct xr_fsm_style *
+get_xr_fsm_style (GtkWidget *w)
+{
+  GtkStyleContext *context = gtk_widget_get_style_context (w);
+  GtkStateFlags state = gtk_widget_get_state_flags (w);
+
+  int xr_width = 500 * 1000;
+
+  PangoFontDescription *pf;
+  gtk_style_context_get (context, state, "font", &pf, NULL);
+
+  struct xr_fsm_style *style = xmalloc (sizeof *style);
+  *style = (struct xr_fsm_style) {
+    .ref_cnt = 1,
+    .size = { [TABLE_HORZ] = xr_width, [TABLE_VERT] = INT_MAX },
+    .min_break = { [TABLE_HORZ] = xr_width / 2, [TABLE_VERT] = 0 },
+    .font = pf,
+    .use_system_colors = true,
+    .object_spacing = XR_POINT * 12,
+    .font_resolution = 96.0,
+  };
+
+  return style;
+}
+
+static GtkBuilder *
+psppire_dialog_action_ctables_activate (PsppireDialogAction *pda, GVariant *param)
+{
+  PsppireDialogActionCtables *act = PSPPIRE_DIALOG_ACTION_CTABLES (pda);
+
+  GtkBuilder *xml = builder_new ("ctables.ui");
+  act->cols_pad = get_widget_assert (xml, "columns-pad");
+  act->rows_pad = get_widget_assert (xml, "rows-pad");
+  act->canvas = get_widget_assert (xml, "template-canvas");
+  g_signal_connect (act->rows_pad, "draw", G_CALLBACK (pad_draw), NULL);
+  g_signal_connect (act->cols_pad, "draw", G_CALLBACK (pad_draw), NULL);
+
+  g_signal_connect (act->canvas, "draw", G_CALLBACK (canvas_draw), pda);
+
+  gtk_drag_dest_set (act->rows_pad, GTK_DEST_DEFAULT_ALL, NULL, 0,
+                     GDK_ACTION_LINK);
+  gtk_drag_dest_set (act->cols_pad, GTK_DEST_DEFAULT_ALL, NULL, 0,
+                     GDK_ACTION_LINK);
+
+  GtkTargetList *tl = gtk_target_list_new (te, 2);
+
+  gtk_drag_dest_add_text_targets (act->rows_pad);
+  gtk_drag_dest_add_text_targets (act->cols_pad);
+
+  gtk_drag_dest_set_target_list (act->rows_pad, tl);
+  gtk_drag_dest_set_target_list (act->cols_pad, tl);
+
+  g_signal_connect (act->rows_pad, "drag-drop", G_CALLBACK (drag_drop_pad), pda);
+  g_signal_connect (act->cols_pad, "drag-drop", G_CALLBACK (drag_drop_pad), pda);
+
+  pda->dialog = get_widget_assert   (xml, "tables-dialog");
+  pda->source = get_widget_assert   (xml, "dict-view");
+
+  gtk_drag_source_set (pda->source, GDK_BUTTON1_MASK, NULL, 0, GDK_ACTION_LINK);
+  gtk_drag_source_set_target_list (pda->source, tl);
+
+  g_signal_connect (pda->source, "drag-begin", G_CALLBACK (drag_begin), pda);
+  g_signal_connect (pda->source, "drag-end", G_CALLBACK (drag_end), pda);
+  g_signal_connect (pda->source, "drag-failed", G_CALLBACK (drag_failed), pda);
+
+  psppire_dialog_action_set_refresh (pda, refresh);
+
+  psppire_dialog_action_set_valid_predicate (pda,
+                                           (ContentsAreValid) dialog_state_valid);
+  return xml;
+}
+
+/*
+  Return  an array of integers which contain the axes of the table.
+  The array is arranged in the order ROW, COLUMN, LAYER.
+  The elements of the array are the indices of the table->dimensions member,
+  which contain that integer.  If there is no such member then the element will
+  be -1.
+
+  In other words, it is the inverse of x: f(x) -> table->dimensions[x], but
+  adjusted to the order ROW, COLUMN, LAYER
+
+  The caller must free this when no longer needed.
+ */
+static size_t *get_dimensions_permutation (const struct pivot_table *table)
+{
+  size_t *perm = xcalloc (PIVOT_N_AXES, sizeof *perm);
+  for (size_t s = 0; s < PIVOT_N_AXES; ++s)
+    perm[s] = -1;
+
+  for (size_t s = 0; s < table->n_dimensions; ++s)
+    {
+      switch (table->dimensions[s]->axis_type)
+        {
+        case PIVOT_AXIS_ROW:
+          perm[0] = s;
+          break;
+        case PIVOT_AXIS_COLUMN:
+          perm[1] = s;
+          break;
+        case PIVOT_AXIS_LAYER:
+          perm[2] = s;
+          break;
+        default:
+          g_assert_not_reached ();
+        }
+    }
+
+  return perm;
+}
+
+static char *
+generate_syntax (const PsppireDialogAction *pda)
+{
+  PsppireDialogActionCtables *act = PSPPIRE_DIALOG_ACTION_CTABLES (pda);
+  const struct pivot_table *table = act->table;
+
+  GString *string = g_string_new ("CTABLES");
+
+  g_string_append (string, " /TABLE");
+
+  size_t *perm = get_dimensions_permutation (table);
+
+  for (size_t idx = 0; idx < PIVOT_N_AXES; ++idx)
+    {
+      if (perm[idx] == -1)
+        continue;
+
+      const struct pivot_dimension *dim = table->dimensions[perm[idx]];
+
+      bool first_variable = true;
+      for (int s = 0; s < dim->root->n_subs; ++s)
+        {
+          const struct pivot_category *sub = dim->root->subs[s];
+
+          if (sub->name->type == PIVOT_VALUE_VARIABLE)
+            {
+              if (idx > 0 && first_variable)
+                {
+                  g_string_append (string, " BY");
+                }
+
+              g_string_append (string, " ");
+              if (!first_variable)
+                g_string_append (string, "+ ");
+              g_string_append (string, sub->name->variable.var_name);
+              first_variable = false;
+            }
+        }
+    }
+
+  free (perm);
+
+  g_string_append (string, ".\n");
+
+  return g_string_free_and_steal (string);
+}
+
+static void
+psppire_dialog_action_ctables_class_init (PsppireDialogActionCtablesClass *class)
+{
+  PSPPIRE_DIALOG_ACTION_CLASS (class)->initial_activate
+    = psppire_dialog_action_ctables_activate;
+
+  PSPPIRE_DIALOG_ACTION_CLASS (class)->generate_syntax = generate_syntax;
+}
+
+static void
+psppire_dialog_action_ctables_init (PsppireDialogActionCtables *act)
+{
+  act->graphic = NULL;
+  act->table = NULL;
+  act->dragged_variable = NULL;
+}
diff --git a/src/ui/gui/psppire-dialog-action-ctables.h b/src/ui/gui/psppire-dialog-action-ctables.h
new file mode 100644 (file)
index 0000000..74fff28
--- /dev/null
@@ -0,0 +1,85 @@
+/* PSPPIRE - a graphical user interface for PSPP.
+   Copyright (C) 2023  Free Software Foundation
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+
+#include <glib-object.h>
+#include <glib.h>
+
+#include "psppire-dialog-action.h"
+
+#ifndef __PSPPIRE_DIALOG_ACTION_CTABLES_H__
+#define __PSPPIRE_DIALOG_ACTION_CTABLES_H__
+
+G_BEGIN_DECLS
+
+
+#define PSPPIRE_TYPE_DIALOG_ACTION_CTABLES (psppire_dialog_action_ctables_get_type ())
+
+#define PSPPIRE_DIALOG_ACTION_CTABLES(obj)     \
+                     (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+                                                 PSPPIRE_TYPE_DIALOG_ACTION_CTABLES, PsppireDialogActionCtables))
+
+#define PSPPIRE_DIALOG_ACTION_CTABLES_CLASS(klass) \
+                     (G_TYPE_CHECK_CLASS_CAST ((klass), \
+                                PSPPIRE_TYPE_DIALOG_ACTION_CTABLES, \
+                                 PsppireDialogActionCtablesClass))
+
+
+#define PSPPIRE_IS_DIALOG_ACTION_CTABLES(obj) \
+                    (G_TYPE_CHECK_INSTANCE_TYPE ((obj), PSPPIRE_TYPE_DIALOG_ACTION_CTABLES))
+
+#define PSPPIRE_IS_DIALOG_ACTION_CTABLES_CLASS(klass) \
+                     (G_TYPE_CHECK_CLASS_TYPE ((klass), PSPPIRE_TYPE_DIALOG_ACTION_CTABLES))
+
+
+#define PSPPIRE_DIALOG_ACTION_CTABLES_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+                                  PSPPIRE_TYPE_DIALOG_ACTION_CTABLES, \
+                                  PsppireDialogActionCtablesClass))
+
+typedef struct _PsppireDialogActionCtables       PsppireDialogActionCtables;
+typedef struct _PsppireDialogActionCtablesClass  PsppireDialogActionCtablesClass;
+
+struct pivot_table;
+struct output_item;
+struct variable;
+
+struct _PsppireDialogActionCtables
+{
+  PsppireDialogAction parent;
+
+  /*< private >*/
+  GtkWidget *cols_pad;
+  GtkWidget *rows_pad;
+  GtkWidget *canvas;
+  struct pivot_table *table;
+  struct output_item *graphic;
+  const struct variable *dragged_variable;
+
+  gboolean dispose_has_run ;
+};
+
+
+struct _PsppireDialogActionCtablesClass
+{
+  PsppireDialogActionClass parent_class;
+};
+
+
+GType psppire_dialog_action_ctables_get_type (void) ;
+
+G_END_DECLS
+
+#endif /* __PSPPIRE_DIALOG_ACTION_CTABLES_H__ */
index 25aff745c4f9913dba123c3331b00b0986987890..c0d3832deb91a37517fd101af64c4d12db348c31 100644 (file)
@@ -71,6 +71,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #include "psppire-dialog-action-select.h"
 #include "psppire-dialog-action-sort.h"
 #include "psppire-dialog-action-split.h"
+#include "psppire-dialog-action-ctables.h"
 #include "psppire-dialog-action-tt1s.h"
 #include "psppire-dialog-action-two-sample.h"
 #include "psppire-dialog-action-univariate.h"
@@ -128,6 +129,7 @@ static const get_type_func dialog_action_types[]=
   psppire_dialog_action_select_get_type,
   psppire_dialog_action_sort_get_type,
   psppire_dialog_action_split_get_type,
+  psppire_dialog_action_ctables_get_type,
   psppire_dialog_action_tt1s_get_type,
   psppire_dialog_action_two_sample_get_type,
   psppire_dialog_action_weight_get_type,