utils_ui.cc 18.3 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Glom
 *
 * Copyright (C) 2001-2004 Murray Cumming
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
17
18
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
19
20
 */

Murray Cumming's avatar
Murray Cumming committed
21
#include "config.h" // For GLOM_ENABLE_CLIENT_ONLY
22
23

#include <glom/utils_ui.h>
24
#include <glom/appwindow.h>
Murray Cumming's avatar
Murray Cumming committed
25
#include <libglom/utils.h>
26
27
28
29
#include <libglom/connectionpool.h>
#include <libglom/data_structure/layout/report_parts/layoutitem_fieldsummary.h>
#include <libglom/data_structure/glomconversions.h>

30
31
32
33
#include <libglom/data_structure/layout/layoutitem_image.h> // For GLOM_IMAGE_FORMAT
#include <gdkmm/pixbufloader.h>
#include <libgda/gda-blob-op.h> // For gda_blob_op_read_all()

34
#include <gtkmm/messagedialog.h>
35
36
37
#include <glibmm/convert.h>
#include <glibmm/miscutils.h>
#include <glibmm/fileutils.h>
38
#include <glibmm/main.h>
39
#include <glibmm/i18n.h>
40
41
42
43
#include <iostream>   // for cout, endl

#include <stack>

44
45
46
47
48
// For ShellExecute:
#ifdef G_OS_WIN32
# include <windows.h>
#endif

49
50
51
namespace
{

52
53
static void on_css_parsing_error(const Glib::RefPtr<const Gtk::CssSection>& section, const Glib::Error& error,
    const Glib::ustring& error_clue)
54
55
{
  std::cerr << G_STRFUNC << ": Parsing error: " << error.what() << std::endl;
56
  std::cerr << "  css: " << error_clue << std::endl;
57
58
59
60
61
62
63
64
65
66
67

  if(section)
  {
    std::cerr << " URI = " << section->get_file()->get_uri() << std::endl;
    std::cerr << " start_line = " << section->get_start_line()+1
      << ", end_line = " << section->get_end_line()+1 << std::endl;
    std::cerr << " start_position = " << section->get_start_position()
      << ", end_position = " << section->get_end_position() << std::endl;
  }
}

68
static Glib::RefPtr<Gtk::CssProvider> create_css_provider(Gtk::Widget& widget, const Glib::ustring& error_clue)
69
70
71
{
  // Add a StyleProvider so we can change the color, background color, and font.
  // This was easier before Gtk::Widget::override_color() was deprecated.
72
  auto css_provider = Gtk::CssProvider::create();
73

74
  auto refStyleContext = widget.get_style_context();
75
76
77
78
  if(refStyleContext)
    refStyleContext->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);

  css_provider->signal_parsing_error().connect(
79
80
81
    sigc::bind(
      sigc::ptr_fun(&on_css_parsing_error),
      error_clue));
82
83
84
85

  return css_provider;
}

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

static void load_into_css_provider(Gtk::Widget& widget, const Glib::ustring& css)
{
  auto css_provider = create_css_provider(widget, css);

  try
  {
    css_provider->load_from_data(css);
  }
  catch(const Gtk::CssProviderError& ex)
  {
    std::cerr << G_STRFUNC << ": Gtk::CssProvider::load_from_data() failed with CssProviderError: "
      << ex.what() << std::endl;
    std::cerr << "  with css: " << css << std::endl;
  }
  catch(const Glib::Error& ex)
  {
    std::cerr << G_STRFUNC << ": Gtk::CssProvider::load_from_data() failed with Error: "
      << ex.what() << std::endl;
    std::cerr << "  with css: " << css << std::endl;
  }
}

109
110
111
112
113
} //anonymous namespace

namespace Glom
{

114
// Run dialog and response on Help if appropriate.
115
int UiUtils::dialog_run_with_help(Gtk::Dialog* dialog, const Glib::ustring& id)
116
{
Murray Cumming's avatar
Murray Cumming committed
117
  int result = dialog->run();
Murray Cumming's avatar
Murray Cumming committed
118

Murray Cumming's avatar
Murray Cumming committed
119
  while (result == Gtk::RESPONSE_HELP)
120
  {
121
    show_help(dialog, id);
122
123
124
125
126
127
128
129
130
131
132
133
134
    result = dialog->run();
  }

  dialog->hide();
  return result;
}

/*
 * Help::show_help(const std::string& id)
 *
 * Launch a help browser with the glom help and load the given id if given
 * If the help cannot be found an error dialog will be shown
 */
135
void UiUtils::show_help(Gtk::Window* parent_window, const Glib::ustring& id)
136
{
137
138
139
  //TODO: Check that this actually works for any dialog that has an ID in the help files.
  Glib::ustring uri = "help:glom";
  if (!id.empty())
140
    uri += "/" + id;
Murray Cumming's avatar
Murray Cumming committed
141

142
143
  try
  {
144
145
146
147
148
149
150
151
    //Use the GNOME help browser:
    GError* gerror = nullptr;
    Glib::RefPtr<Gdk::Screen> screen;
    if(parent_window)
      screen = parent_window->get_screen();

    if(!gtk_show_uri(screen ? screen->gobj() : nullptr,
      uri.c_str(), GDK_CURRENT_TIME, &gerror))
152
    {
153
154
155
156
157
      std::cerr << G_STRFUNC << ": " << gerror->message << std::endl;

      const Glib::ustring message(gerror->message);
      g_error_free(gerror);
      throw std::runtime_error(message);
158
159
160
161
    }
  }
  catch(const std::exception& ex)
  {
Murray Cumming's avatar
Murray Cumming committed
162
    const auto message = Glib::ustring::compose(_("Could not display help: %1"), Glib::ustring(ex.what()));
163
164
165
166
167
    Gtk::MessageDialog dialog(message, false, Gtk::MESSAGE_ERROR);
    dialog.run();
  }
}

168
void UiUtils::show_ok_dialog(const Glib::ustring& title, const Glib::ustring& message, Gtk::Window* parent, Gtk::MessageType message_type)
169
170
171
172
173
174
175
176
177
{
  Gtk::MessageDialog dialog("<b>" + title + "</b>", true /* markup */, message_type, Gtk::BUTTONS_OK);
  dialog.set_secondary_text(message);
  if(parent)
    dialog.set_transient_for(*parent);

  dialog.run();
}

178
void UiUtils::show_ok_dialog(const Glib::ustring& title, const Glib::ustring& message, Gtk::Window& parent, Gtk::MessageType message_type)
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
{
  show_ok_dialog(title, message, &parent, message_type);
}

namespace
{

static void on_window_hide(Glib::RefPtr<Glib::MainLoop> main_loop, sigc::connection handler_connection)
{
  handler_connection.disconnect(); //This should release a main_loop reference.
  main_loop->quit();

  //main_loop should be destroyed soon, because nothing else is using it.
}

} //anonymous namespace.

196
void UiUtils::show_window_until_hide(Gtk::Window* window)
197
198
199
200
{
  if(!window)
    return;

201
  auto main_loop = Glib::MainLoop::create(false /* not running */);
202
203
204

  //Stop the main_loop when the window is hidden:
  sigc::connection handler_connection; //TODO: There seems to be a crash if this is on the same line.
Murray Cumming's avatar
Murray Cumming committed
205
  handler_connection = window->signal_hide().connect(
206
207
208
209
    sigc::bind(
      sigc::ptr_fun(&on_window_hide),
      main_loop, handler_connection
    ) );
Murray Cumming's avatar
Murray Cumming committed
210

211
212
213
214
  window->show();
  main_loop->run(); //Run and block until it is stopped by the hide signal handler.
}

215
Glib::ustring UiUtils::bold_message(const Glib::ustring& message)
216
217
218
219
220
{
  return "<b>" + message + "</b>";
}


221
Glib::RefPtr<Gdk::Pixbuf> UiUtils::get_pixbuf_for_gda_value(const Gnome::Gda::Value& value)
222
223
224
225
226
227
228
229
230
{
  Glib::RefPtr<Gdk::Pixbuf> result;

  if(value.get_value_type() == GDA_TYPE_BINARY || value.get_value_type() == GDA_TYPE_BLOB)
  {
    glong buffer_binary_length;
    gconstpointer buffer_binary;
    if(value.get_value_type() == GDA_TYPE_BLOB)
    {
Murray Cumming's avatar
Murray Cumming committed
231
      const auto blob = value.get_blob();
232
233
234
235
236
237
238
239
      if(gda_blob_op_read_all(blob->op, const_cast<GdaBlob*>(blob)))
      {
        buffer_binary_length = blob->data.binary_length;
        buffer_binary = blob->data.data;
      }
      else
      {
        buffer_binary_length = 0;
240
        buffer_binary = 0;
241
        std::cerr << G_STRFUNC << ": Failed to read BLOB data\n";
242
243
244
245
246
247
248
249
250
251
252
      }
    }
    else
    {
      buffer_binary = value.get_binary(buffer_binary_length);
    }

    /* Note that this is regular binary data, not escaped text representing the binary data: */
    if(buffer_binary && buffer_binary_length)
    {
      //typedef std::list<Gdk::PixbufFormat> type_list_formats;
Murray Cumming's avatar
Murray Cumming committed
253
      //const auto formats = Gdk::Pixbuf::get_formats();
254
      //std::cout << "Debug: Supported pixbuf formats:\n";
255
      //for(const auto& item : formats)
256
257
258
259
      //{
      //  std::cout << " name=" << iter->get_name() << ", writable=" << iter->is_writable() << std::endl;
      //}

Murray Cumming's avatar
Murray Cumming committed
260
      Glib::RefPtr<Gdk::PixbufLoader> refPixbufLoader;
261
262
      try
      {
Armin Burgmeier's avatar
Armin Burgmeier committed
263
        refPixbufLoader = Gdk::PixbufLoader::create();
264
265
266
267
      }
      catch(const Gdk::PixbufError& ex)
      {
        refPixbufLoader.reset();
268
        std::cerr << G_STRFUNC << ": PixbufLoader::create failed: " << ex.what() << std::endl;
269
270
271
272
273
274
275
      }

      if(refPixbufLoader)
      {
        guint8* puiData = (guint8*)buffer_binary;
        try
        {
276
          refPixbufLoader->write(puiData, static_cast<gsize>(buffer_binary_length));
277
278
279
280
281
282
283
          result = refPixbufLoader->get_pixbuf();

          refPixbufLoader->close(); //This throws if write() threw, so it must be inside the try block.
        }

        catch(const Glib::Exception& ex)
        {
284
          std::cerr << G_STRFUNC << ": PixbufLoader::write() failed: " << ex.what() << std::endl;
285
286
287
288
289
290
291
292
293
294
295
296
        }
      }

      //TODO: load the image, using the mime type stored elsewhere.
      //pixbuf = Gdk::Pixbuf::create_from_data(
    }

  }

  return result;
}

297
298
namespace {

299
300
301
static int get_width_for_text(Gtk::Widget& widget, const Glib::ustring& text)
{
  //Get the width required for this string in the current font:
302
  auto refLayout = widget.create_pango_layout(text);
303
304
305
306
307
308
309
310
311
312
313
  int width = 0;
  int height = 0;
  refLayout->get_pixel_size(width, height);
  int result = width;

  //Add a bit more:
  result += 10;

  return result;
}

Murray Cumming's avatar
Murray Cumming committed
314
} //anonymous namespace.
315

Murray Cumming's avatar
Murray Cumming committed
316
int UiUtils::get_suitable_field_width_for_widget(Gtk::Widget& widget, const std::shared_ptr<const LayoutItem_Field>& field_layout, bool or_title, bool for_treeview)
317
{
318
  int result = 150; //Suitable default.
319

Murray Cumming's avatar
Murray Cumming committed
320
  const auto field_type = field_layout->get_glom_type();
321
322
323
324

  Glib::ustring example_text;
  switch(field_type)
  {
325
    case(Field::glom_field_type::DATE):
326
    {
327
      const Glib::Date date(31, Glib::Date::Month(12), 2000);
328
329
330
      example_text = Conversions::get_text_for_gda_value(field_type, Gnome::Gda::Value(date));
      break;
    }
331
    case(Field::glom_field_type::TIME):
332
333
334
335
336
337
338
339
    {
      Gnome::Gda::Time time = {0, 0, 0, 0, 0};
      time.hour = 24;
      time.minute = 59;
      time.second = 59;
      example_text = Conversions::get_text_for_gda_value(field_type, Gnome::Gda::Value(time));
      break;
    }
340
    case(Field::glom_field_type::NUMERIC):
341
    {
342
343
344
345
      if(for_treeview)
        example_text = "EUR 999.99";
      else
        example_text = "EUR 9999999999";
Murray Cumming's avatar
Murray Cumming committed
346

347
348
      break;
    }
349
350
    case(Field::glom_field_type::TEXT):
    case(Field::glom_field_type::IMAGE): //Give images the same width as text fields, so they will often line up.
351
    {
352
353
354
      if(for_treeview)
        example_text = "AAAAAAAAAAAA";
      else
355
        example_text = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
356

357
358
359
360
361
362
363
364
365
366
367
      break;
    }
    default:
    {
      break;
    }
  }

  if(!example_text.empty())
  {
    //Get the width required for this string in the current font:
368
    result = get_width_for_text(widget, example_text);
369
370
371
372
373
  }

  if(or_title)
  {
    //Make sure that there's enough space for the title too.
Murray Cumming's avatar
Murray Cumming committed
374
    const auto title_width = get_width_for_text(widget, item_get_title(field_layout));
375
376
    if(title_width > result)
      result = title_width;
377
378
379
380
381
382
  }

  return result;
}


383
std::string UiUtils::get_filepath_with_extension(const std::string& filepath, const std::string& extension)
384
385
386
387
388
389
390
391
392
393
394
395
{
  std::string result = filepath;

  bool add_ext = false;
  const std::string str_ext = "." + extension;

  if(result.size() < str_ext.size()) //It can't have the ext already if it's not long enough.
  {
    add_ext = true; //It isn't there already.
  }
  else
  {
Murray Cumming's avatar
Murray Cumming committed
396
    const auto strEnd = result.substr(result.size() - str_ext.size());
397
398
399
400
401
402
403
404
405
406
407
408
409
    if(strEnd != str_ext) //If it doesn't already have the extension
      add_ext = true;
  }

  //Add extension if necessay.
  if(add_ext)
    result += str_ext;

  //TODO: Do not replace existing extensions, so it could be e.g. 'something.blah.theext'

  return result;
}

Murray Cumming's avatar
Merged.    
Murray Cumming committed
410

411
//static:
412
Glib::RefPtr<Gdk::Pixbuf> UiUtils::image_scale_keeping_ratio(const Glib::RefPtr<Gdk::Pixbuf>& pixbuf, int target_height, int target_width)
413
414
415
416
417
418
419
{
  if( (target_height == 0) || (target_width == 0) )
    return Glib::RefPtr<Gdk::Pixbuf>(); //This shouldn't happen anyway.

  if(!pixbuf)
    return pixbuf;

420
  enum class enum_scale_mode
421
  {
422
423
424
425
    WIDTH,
    HEIGHT,
    BOTH,
    NONE
426
427
  };

428
  enum_scale_mode scale_mode = enum_scale_mode::NONE; //Start with either the width or height, and scale the other according to the ratio.
429

Murray Cumming's avatar
Murray Cumming committed
430
431
  const auto pixbuf_height = pixbuf->get_height();
  const auto pixbuf_width = pixbuf->get_width();
432
433
434
435
436

  if(pixbuf_height > target_height)
  {
    if(pixbuf_width > target_width)
    {
437
      scale_mode = enum_scale_mode::BOTH;
438
439
440
441
    }
    else
    {
      //Only the height is bigger:
442
      scale_mode = enum_scale_mode::HEIGHT;
443
444
445
446
447
    }
  }
  else if(pixbuf_width > target_width)
  {
    //Only the height is bigger:
448
    scale_mode = enum_scale_mode::WIDTH;
449
450
  }

451
  if(scale_mode == enum_scale_mode::NONE)
452
    return pixbuf;
453
  else if(scale_mode == enum_scale_mode::HEIGHT)
454
  {
Murray Cumming's avatar
Murray Cumming committed
455
    const float ratio = (float)target_height / (float)pixbuf_height;
456
457
    target_width = (int)((float)pixbuf_width * ratio);
  }
458
  else if(scale_mode == enum_scale_mode::WIDTH)
459
460
461
  {
    const float ratio = (float)target_width / (float) pixbuf_width;
    target_height = (int)((float)pixbuf_height * ratio);
462
  }
463
  else if(scale_mode == enum_scale_mode::BOTH)
464
  {
Murray Cumming's avatar
Murray Cumming committed
465
    const auto ratio = std::min(
466
467
468
469
      (float)target_width / (float) pixbuf_width,
      (float)target_height / (float) pixbuf_height);
    target_width = (int)((float)pixbuf_width * ratio);
    target_height = (int)((float)pixbuf_height * ratio);
470
471
472
473
474
475
476
477
478
479
  }

 if( (target_height == 0) || (target_width == 0) )
 {
   return Glib::RefPtr<Gdk::Pixbuf>(); //This shouldn't happen anyway. It seems to happen sometimes though, when ratio is very small.
 }

  return pixbuf->scale_simple(target_width, target_height, Gdk::INTERP_NEAREST);
}

480
bool UiUtils::show_warning_no_records_found(Gtk::Window& transient_for)
481
{
Murray Cumming's avatar
Murray Cumming committed
482
  const auto message = _("Your find criteria did not match any records in the table.");
483

484
  Gtk::MessageDialog dialog(UiUtils::bold_message(_("No Records Found")), true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE);
485
486
  dialog.set_secondary_text(message);
  dialog.set_transient_for(transient_for);
487

488
  dialog.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
489
490
  dialog.add_button(_("New Find"), Gtk::RESPONSE_OK);

Murray Cumming's avatar
Murray Cumming committed
491
  const auto find_again = (dialog.run() == Gtk::RESPONSE_OK);
492
493
494
  return find_again;
}

495

496
void UiUtils::show_report_in_browser(const std::string& filepath, Gtk::Window* parent_window)
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
{
  //Give the user a clue, in case the web browser opens in the background, for instance in a new tab:
  if(parent_window)
    show_ok_dialog(_("Report Finished"), _("The report will now be opened in your web browser."), *parent_window, Gtk::MESSAGE_INFO);

#ifdef G_OS_WIN32
  // gtk_show_uri doesn't seem to work on Win32, at least not for local files
  // We use Windows API instead.
  // TODO: Check it again and file a bug if necessary.
  ShellExecute(0, "open", filepath.c_str(), 0, 0, SW_SHOW);
#else

  Glib::ustring uri;
  try
  {
    uri = Glib::filename_to_uri(filepath);
  }
  catch(const Glib::ConvertError& ex)
  {
    std::cerr << G_STRFUNC << ": Could not convert filepath to URI: " << filepath << std::endl;
    return;
  }

  //Use the GNOME browser:
Murray Cumming's avatar
Murray Cumming committed
521
  GError* gerror = nullptr;
522
523
524
525
526
527
  Glib::RefPtr<Gdk::Screen> screen;
  if(parent_window)
    screen = parent_window->get_screen();

  if(!gtk_show_uri(screen ? screen->gobj() : nullptr,
    uri.c_str(), GDK_CURRENT_TIME, &gerror))
528
529
530
531
532
533
534
  {
    std::cerr << G_STRFUNC << ": " << gerror->message << std::endl;
    g_error_free(gerror);
  }
#endif //G_OS_WIN32
}

535
std::string UiUtils::get_icon_path(const Glib::ustring& filename)
536
{
537
  return  "/org/gnome/glom/data/icons/" + filename;
538
539
}

540
bool UiUtils::script_check_for_pygtk2_with_warning(const Glib::ustring& script, Gtk::Window* parent_window)
541
542
543
{
  if(!Utils::script_check_for_pygtk2(script))
  {
544
    UiUtils::show_ok_dialog(_("Script Uses PyGTK 2"),
545
546
547
548
549
550
551
      _("Glom cannot run this script because it uses pygtk 2, but Glom uses GTK+ 3, and attempting to use pygtk 2 would cause Glom to crash."), parent_window, Gtk::MESSAGE_ERROR);
    return false;
  }

  return true;
}

552
void UiUtils::treeview_delete_all_columns(Gtk::TreeView* treeview)
553
554
555
556
557
558
559
560
561
562
563
{
  if(!treeview)
    return;

  //We use this instead of just Gtk::TreeView::remove_all_columns()
  //because that deletes columns as a side-effect of unreferencing them,
  //and that behaviour might be fixed in gtkmm sometime,
  //and whether they should be deleted by that would depend on whether we used Gtk::manage().
  //Deleting them explicitly is safer and clearer. murrayc.

  //Remove all View columns:
564
565
  auto vecViewColumns = treeview->get_columns();
  for (auto& view_column : vecViewColumns)
566
  {
567
    if(!view_column)
568
569
      continue;

Murray Cumming's avatar
Murray Cumming committed
570
    GtkTreeViewColumn* weak_ptr = nullptr;
571
    g_object_add_weak_pointer (G_OBJECT (view_column->gobj()), (gpointer*)&weak_ptr);
572
573
574
575
576

    //Keep the object alive, instead of letting gtk_tree_view_remove_column() delete it by reducing its reference to 0,
    //so we can explicitly delete it.
    //This feels safer, considering some strange crashes I've seen when using Gtk::TreeView::remove_all_columns(),
    //though that might have been just because we didn't reset m_treeviewcolumn_button. murrayc.
577
578
579
    view_column->reference();
    treeview->remove_column(*view_column);
    delete view_column; //This should cause it to be removed.
580
581
582

    if(weak_ptr)
    {
583
      std::cerr << G_STRFUNC << ": The GtkTreeViewColumn was not destroyed as expected.\n";
584
585
586
587
    }
  }
}

588
589
590
591
void UiUtils::container_remove_all(Gtk::Container& container)
{
  //Remove all (usally just one) widgets from m_vbox_parent:
  //Gtk::Bin::remove() is easier but after GtkAlignment was deprecated, there is no suitable widget.
592
  for(const auto& child : container.get_children())
593
594
  {
    if(child)
595
      container.remove(*child);
596
597
598
  }
}

599
600
void UiUtils::load_font_into_css_provider(Gtk::Widget& widget, const Glib::ustring& font)
{
601
602
  const auto css = "* { font: " + font + "; }";
  load_into_css_provider(widget, css);
603
604
605
606
}

void UiUtils::load_color_into_css_provider(Gtk::Widget& widget, const Glib::ustring& color)
{
607
608
  const auto css = "* { color: " + color + "; }";
  load_into_css_provider(widget, css);
609
610
611
612
}

void UiUtils::load_background_color_into_css_provider(Gtk::Widget& widget, const Glib::ustring& color)
{
613
  auto css = "* { background-color: " + color + "; }";
614
615
616
617
618
619
620
621
622
623
624
/*
      "GtkTextView {\n"
      "  background-color: " + color + "; }\n"
      "GtkTextView:backdrop {\n"
      "  background-color: " + color + "; }\n"
      "GtkTextView.view:selected {\n"
      "  background-color: " + color + "; }\n"
      "GtkTextView.view:insensitive {\n"
      "  background-color: " + color + "; }"
*/

625
  load_into_css_provider(widget, css);
626
627
}

628
} //namespace Glom