progress.*: cosmetix
[k8lowj.git] / src / jamdoc.c
blobe82b9a53d02eca848df7b5926bd09533968da285
1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2003 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
5 */
6 #include "glib-all.h"
8 #include <string.h>
10 #include "account.h"
11 #include "conf.h"
12 #include "draftstore.h"
13 #include "jamdoc.h"
14 #include "util.h"
16 #ifdef HAVE_GTK
17 # include "get_cmd_out.h"
18 # include "smartquotes.h"
19 #endif
22 enum {
23 ENTRY_CHANGED,
24 UPDATE_DOC,
25 LAST_SIGNAL
29 enum {
30 PROP_0,
31 PROP_DIRTY,
32 PROP_USEJOURNAL,
33 PROP_ACCOUNT
37 struct _JamDoc {
38 GObject parent;
40 /* for drafts, the current entry is tracked through entry->itemid.
41 * for files, we need to keep the filename around. */
42 char *filename;
43 /* we only save as xml, so we need to remember whether they opened
44 * a non-xml file. */
45 gboolean filename_not_xml;
46 /* the convention is:
47 * if there's a filename, we're editing a file.
48 * if itemid is less than zero, we're editing a draft.
49 * otherwise, it's an untitled/unattached doc.
52 /* if autosave is on, each document gets its own autosave file.
53 * this is not related to the drafts mentioned above. */
54 char *autosave_filename;
56 gboolean dirty;
58 LJEntry *entry;
59 #ifdef HAVE_GTK
60 GtkTextBuffer *buffer;
61 #endif /* HAVE_GTK */
62 guint buffer_signal;
63 gchar *usejournal;
65 JamAccount *account;
67 gchar *url; /* entry URL; valid only after posting */
71 struct _JamDocClass {
72 GObjectClass parent_class;
73 void (*entry_changed) (JamDoc *doc);
74 void (*update_doc) (JamDoc *doc);
78 static void jam_doc_update_doc (JamDoc *doc);
79 static void jam_doc_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
80 static void jam_doc_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
82 static guint signals[LAST_SIGNAL] = { 0 };
85 static void jam_doc_class_init (gpointer klass, gpointer class_data) {
86 GObjectClass *gclass = G_OBJECT_CLASS(klass);
87 gclass->set_property = jam_doc_set_property;
88 gclass->get_property = jam_doc_get_property;
89 signals[ENTRY_CHANGED] = g_signal_new("entry_changed",
90 LOGJAM_TYPE_DOC,
91 G_SIGNAL_RUN_LAST,
92 G_STRUCT_OFFSET(JamDocClass, entry_changed),
93 NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
94 signals[UPDATE_DOC] = g_signal_new("update_doc",
95 LOGJAM_TYPE_DOC,
96 G_SIGNAL_RUN_LAST,
97 G_STRUCT_OFFSET(JamDocClass, update_doc), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
98 g_object_class_install_property(gclass, PROP_DIRTY,
99 g_param_spec_boolean("dirty", "Is dirty", "If TRUE, the document has changed since it was last saved.", FALSE, G_PARAM_READWRITE));
100 g_object_class_install_property(gclass, PROP_USEJOURNAL,
101 g_param_spec_string("usejournal", "use journal", "The current journal that the account is working with.", NULL, G_PARAM_READWRITE));
102 g_object_class_install_property(gclass, PROP_ACCOUNT,
103 g_param_spec_pointer("account", "account", "The current account that the user is working with.", G_PARAM_READWRITE));
107 #ifdef HAVE_GTK
108 static void buffer_changed_cb (GtkTextBuffer *buf, JamDoc *doc) {
109 if (gtk_text_buffer_get_modified(buf)) jam_doc_set_dirty(doc, TRUE);
113 GtkTextBuffer *jam_doc_get_text_buffer (JamDoc *doc) {
114 return doc->buffer;
116 #endif /* HAVE_GTK */
119 static void jam_doc_init (GTypeInstance *inst, gpointer g_class) {
120 JamDoc *doc = LOGJAM_DOC(inst);
121 doc->entry = lj_entry_new();
122 doc->entry->security = conf.defaultsecurity;
123 doc->dirty = FALSE;
124 #ifdef HAVE_GTK
125 doc->buffer = gtk_text_buffer_new(NULL);
126 doc->buffer_signal = g_signal_connect(G_OBJECT(doc->buffer), "modified-changed", G_CALLBACK(buffer_changed_cb), doc);
127 gtk_text_buffer_set_modified(doc->buffer, FALSE);
128 if (conf.options.smartquotes) smartquotes_attach(doc->buffer, conf.options.smartquotes_russian);
129 #endif /* HAVE_GTK */
130 doc->url = NULL;
134 GType jam_doc_get_type (void) {
135 static GType doc_type = 0;
136 if (!doc_type) {
137 static const GTypeInfo doc_info = {
138 sizeof(JamDocClass),
139 NULL,
140 NULL,
141 jam_doc_class_init,
142 NULL,
143 NULL,
144 sizeof (JamDoc),
146 jam_doc_init
148 doc_type = g_type_register_static(G_TYPE_OBJECT, "JamDoc", &doc_info, 0);
150 return doc_type;
154 static void jam_doc_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) {
155 /* unimplemented. */
159 static void jam_doc_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) {
160 /* unimplemented. */
164 JamDoc *jam_doc_new (void) {
165 return LOGJAM_DOC(g_object_new(jam_doc_get_type(), NULL));
169 void jam_doc_set_dirty (JamDoc *doc, gboolean dirty) {
170 if (doc->dirty == dirty) return;
171 doc->dirty = dirty;
172 #ifdef HAVE_GTK
173 if (doc->dirty == FALSE) gtk_text_buffer_set_modified(doc->buffer, FALSE);
174 #endif /* HAVE_GTK */
175 g_object_notify(G_OBJECT(doc), "dirty");
179 gboolean jam_doc_get_dirty (JamDoc *doc) { return doc->dirty; }
181 const char *jam_doc_get_subject (JamDoc *doc) { return doc->entry->subject; }
182 void jam_doc_set_subject (JamDoc *doc, const char *subject) { string_replace(&doc->entry->subject, subject ? g_strdup(subject) : NULL); }
184 const char *jam_doc_get_mood (JamDoc *doc) { return doc->entry->mood; }
185 void jam_doc_set_mood (JamDoc *doc, const char *mood) { string_replace(&doc->entry->mood, mood ? g_strdup(mood) : NULL); }
186 void jam_doc_set_moodid (JamDoc *doc, int moodid) { doc->entry->moodid = moodid; }
188 const char *jam_doc_get_music (JamDoc *doc) { return doc->entry->music; }
189 void jam_doc_set_music (JamDoc *doc, const char *music) { string_replace(&doc->entry->music, music ? g_strdup(music) : NULL); }
191 const char *jam_doc_get_location (JamDoc *doc) { return doc->entry->location; }
192 void jam_doc_set_location (JamDoc *doc, const char *location) { string_replace(&doc->entry->location, location ? g_strdup(location) : NULL); }
194 const char *jam_doc_get_taglist (JamDoc *doc) { return doc->entry->taglist; }
195 void jam_doc_set_taglist (JamDoc *doc, const char *taglist) { string_replace(&doc->entry->taglist, taglist ? g_strdup(taglist) : NULL); }
197 LJSecurity jam_doc_get_security (JamDoc *doc) { return doc->entry->security; }
198 void jam_doc_set_security (JamDoc *doc, LJSecurity *sec) { doc->entry->security = *sec; }
200 LJCommentsType jam_doc_get_comments (JamDoc *doc) { return doc->entry->comments; }
201 void jam_doc_set_comments (JamDoc *doc, LJCommentsType type) { doc->entry->comments = type; }
203 LJScreeningType jam_doc_get_screening (JamDoc *doc) { return doc->entry->screening; }
204 void jam_doc_set_screening (JamDoc *doc, LJScreeningType type) { doc->entry->screening = type; }
206 void jam_doc_get_time (JamDoc *doc, struct tm *ptm) { *ptm = doc->entry->time; }
207 void jam_doc_set_time (JamDoc *doc, const struct tm *ptm) { if (ptm) doc->entry->time = *ptm; else memset((void *)&doc->entry->time, 0, sizeof(struct tm)); }
209 gboolean jam_doc_get_backdated (JamDoc *doc) { return doc->entry->backdated; }
210 void jam_doc_set_backdated (JamDoc *doc, gboolean backdated) { doc->entry->backdated = backdated; }
212 gboolean jam_doc_get_preformatted (JamDoc *doc) { return doc->entry->preformatted; }
213 void jam_doc_set_preformatted (JamDoc *doc, gboolean preformatted) { doc->entry->preformatted = preformatted; }
215 void jam_doc_set_event (JamDoc *doc, const char *event) {
216 g_free(doc->entry->event);
217 doc->entry->event = (event ? g_strdup(event) : NULL);
220 const char *jam_doc_get_picture (JamDoc *doc) { return doc->entry->pickeyword; }
221 void jam_doc_set_picture (JamDoc *doc, const char *picture) { string_replace(&doc->entry->pickeyword, picture ? g_strdup(picture) : NULL); }
223 void jam_doc_set_pickeyword (JamDoc *doc, const char *keyword) { jam_doc_set_picture(doc, keyword); }
226 static void set_entry (JamDoc *doc, LJEntry *entry) {
227 #ifdef HAVE_GTK
228 GtkTextIter start, end;
229 #endif /* HAVE_GTK */
230 lj_entry_free(doc->entry);
231 if (entry) doc->entry = entry; else entry = doc->entry = lj_entry_new();
232 #ifdef HAVE_GTK
233 /* block the buffer signal so we don't rapidly flip to dirty and back. */
234 g_signal_handler_block(doc->buffer, doc->buffer_signal);
235 gtk_text_buffer_get_bounds(doc->buffer, &start, &end);
236 gtk_text_buffer_delete(doc->buffer, &start, &end);
237 if (entry->event) gtk_text_buffer_insert(doc->buffer, &start, entry->event, -1);
238 g_signal_handler_unblock(doc->buffer, doc->buffer_signal);
239 #endif /* HAVE_GTK */
240 jam_doc_set_dirty(doc, FALSE);
241 #ifdef HAVE_GTK
242 /* since the buffer signal was blocked, we need to do this manually. */
243 gtk_text_buffer_set_modified(doc->buffer, FALSE);
244 #endif /* HAVE_GTK */
248 gboolean jam_doc_load_file (JamDoc *doc, const char *filename, LJEntryFileType type, GError **err) {
249 LJEntry *entry;
250 entry = lj_entry_new_from_filename(filename, type, &type, err);
251 if (!entry) return FALSE;
252 entry->itemid = 0;
253 set_entry(doc, entry);
254 string_replace(&doc->filename, g_strdup(filename));
255 doc->filename_not_xml = (type != LJ_ENTRY_FILE_XML);
256 g_signal_emit_by_name(doc, "entry_changed");
257 return TRUE;
261 void jam_doc_load_draft (JamDoc *doc, LJEntry *entry) {
262 set_entry(doc, lj_entry_copy(entry));
263 g_signal_emit_by_name(doc, "entry_changed");
267 void jam_doc_load_entry (JamDoc *doc, LJEntry *entry) {
268 set_entry(doc, lj_entry_copy(entry));
269 g_signal_emit_by_name(doc, "entry_changed");
273 static gboolean save_draft(LJEntry *entry, JamAccount *acc, GError **err) {
274 DraftStore *ds = draft_store_new(acc);
275 if (entry->itemid == 0) entry->itemid = draft_store_find_itemid(ds);
276 if (!draft_store_put_entry(ds, entry, err)) {
277 draft_store_free(ds);
278 return FALSE;
280 draft_store_free(ds);
281 return TRUE;
285 gboolean jam_doc_would_save_over_nonxml (JamDoc *doc) { return (doc->filename && doc->filename_not_xml); }
286 gboolean jam_doc_has_save_target (JamDoc *doc) { return (doc->filename || doc->entry->itemid < 0); } /* filename or a draft. */
289 gboolean jam_doc_save (JamDoc *doc, JamAccount *acc, GError **err) {
290 jam_doc_update_doc(doc);
291 if (doc->filename) {
292 if (!lj_entry_to_xml_file(doc->entry, doc->filename, err)) return FALSE;
293 } else if (doc->entry->itemid < 0) {
294 if (!save_draft(doc->entry, acc, err)) return FALSE;
295 } else {
296 g_error("jam_doc_save: shouldn't be called without savetarget.\n");
297 return FALSE;
299 jam_doc_set_dirty(doc, FALSE);
300 return TRUE;
304 gboolean jam_doc_save_as_file (JamDoc *doc, const char *filename, GError **err) {
305 LJEntry *entry = jam_doc_get_entry(doc);
306 /* always save to files without an itemid. */
307 entry->itemid = 0;
308 if (!lj_entry_to_xml_file(entry, filename, err)) {
309 lj_entry_free(entry);
310 return FALSE;
312 lj_entry_free(entry);
313 doc->entry->itemid = 0;
314 string_replace(&doc->filename, g_strdup(filename));
315 jam_doc_set_dirty(doc, FALSE);
316 return TRUE;
320 gboolean jam_doc_save_as_draft (JamDoc *doc, const char *title, JamAccount *acc, GError **err) {
321 LJEntry *entry = jam_doc_get_entry(doc);
322 string_replace(&entry->subject, g_strdup(title));
323 entry->itemid = 0;
324 if (!save_draft(entry, acc, err)) {
325 lj_entry_free(entry);
326 return FALSE;
328 /* retrieve the new draft id. */
329 doc->entry->itemid = entry->itemid;
330 lj_entry_free(entry);
331 string_replace(&doc->filename, NULL);
332 jam_doc_set_dirty(doc, FALSE);
333 return TRUE;
337 char *jam_doc_get_title (JamDoc *doc) {
338 if (doc->filename) {
339 const gchar *homedir = g_get_home_dir();
340 int homelen = (int)strlen(homedir);
341 if (strncmp(homedir, doc->filename, homelen) == 0) return g_build_filename("~", doc->filename + homelen, NULL);
342 return g_strdup(doc->filename);
343 } else if (doc->entry->itemid < 0) {
344 return g_strdup_printf(_("Draft %d"), -doc->entry->itemid);
345 } else if (doc->entry->itemid > 0) {
346 return g_strdup_printf(_("Entry %d"), doc->entry->itemid);
347 } else {
348 return g_strdup(_("New Entry"));
353 char *jam_doc_get_draftname (JamDoc *doc) {
354 jam_doc_update_doc(doc);
355 if (doc->entry->subject) return g_strdup(doc->entry->subject);
356 return NULL;
360 static void jam_doc_update_doc (JamDoc *doc) {
361 /* XXX this is sorta weird; we emit a signal to get our internal version
362 * of the entry in sync with the state of the widgets. this is important
363 * in decoupling the widgets from the document, but it feels a little
364 * backwards. */
365 g_signal_emit_by_name(doc, "update_doc");
369 LJEntry *jam_doc_get_entry (JamDoc *doc) {
370 jam_doc_update_doc(doc);
371 return lj_entry_copy(doc->entry);
375 #ifdef HAVE_GTK
376 gboolean jam_doc_append_text (JamDoc *doc, const char *text, const char *encoding) {
377 if (text != NULL) {
378 GError *err = NULL;
379 GtkTextIter pos;
380 gtk_text_buffer_get_end_iter(doc->buffer, &pos);
381 gtk_text_buffer_insert(doc->buffer, &pos, text, -1);
382 if (strcmp(encoding, "UTF-8") == 0) {
383 const gchar *end;
384 if (!g_utf8_validate(text, strlen(text), &end)) return FALSE;
385 gtk_text_buffer_insert(doc->buffer, &pos, text, -1);
386 } else {
387 char *newtext;
388 newtext = g_convert(text, -1, "UTF-8", encoding, NULL, NULL, &err);
389 if (err) g_free(err);
390 if (!newtext) return FALSE;
391 gtk_text_buffer_insert(doc->buffer, &pos, newtext, -1);
392 g_free(newtext);
395 return TRUE;
399 gboolean jam_doc_insert_file (JamDoc *doc, const char *filename, const char *encoding, GError **err) {
400 char *text;
401 gsize len;
403 if (!g_file_get_contents(filename, &text, &len, err)) return FALSE;
405 if (strcmp(encoding, "UTF-8") == 0) {
406 const gchar *end;
407 if (!g_utf8_validate(text, len, &end)) {
408 g_set_error(err, 0, 0, _("Invalid UTF-8 starting at byte %d."), end - text);
409 g_free(text);
410 return FALSE;
412 gtk_text_buffer_insert_at_cursor(doc->buffer, text, -1);
413 } else {
414 char *newtext;
415 newtext = g_convert(text, -1, "UTF-8", encoding, NULL, NULL, err);
416 if (!newtext) {
417 g_free(text);
418 return FALSE;
420 gtk_text_buffer_insert_at_cursor(doc->buffer, newtext, -1);
421 g_free(newtext);
423 g_free(text);
424 return TRUE;
428 gboolean jam_doc_insert_command_output (JamDoc *doc, const char *command, const char *encoding, GError **err, GtkWindow *parent) {
429 /* FIXME: encoding is not currently used, instead we recode from
430 * the current locale. Does anybody knows a program which output
431 * is NOT in the current locale's encoding? If yes, let me know.
433 const gchar *end;
434 GString *output = get_command_output(command, err, parent);
435 if (output == NULL) return FALSE;
436 if (g_utf8_validate(output->str, output->len, &end)) {
437 gtk_text_buffer_insert_at_cursor(doc->buffer, output->str, output->len);
438 } else {
439 gchar *newtext = g_locale_to_utf8(output->str, output->len, NULL, NULL, err);
440 if (!newtext) {
441 g_string_free(output, TRUE);
442 return FALSE;
444 gtk_text_buffer_insert_at_cursor(doc->buffer, newtext, -1);
445 g_free(newtext);
447 g_string_free(output, TRUE);
448 return TRUE;
450 #endif /* HAVE_GTK */
453 int jam_doc_get_flags (JamDoc *doc) {
454 LJEntryType type = jam_doc_get_entry_type(doc);
455 int flags = 0;
456 switch (type) {
457 case ENTRY_DRAFT: flags = LOGJAM_DOC_CAN_DELETE|LOGJAM_DOC_CAN_SUBMIT; break;
458 case ENTRY_NEW: flags = LOGJAM_DOC_CAN_SUBMIT; break;
459 case ENTRY_SERVER: flags = LOGJAM_DOC_CAN_DELETE|LOGJAM_DOC_CAN_SAVE; break;
461 return flags;
465 LJEntryType jam_doc_get_entry_type (JamDoc *doc) {
466 gint itemid = jam_doc_get_entry_itemid(doc);
467 return (itemid < 0 ? ENTRY_DRAFT : itemid == 0 ? ENTRY_NEW : ENTRY_SERVER);
471 gint jam_doc_get_entry_itemid (JamDoc *doc) {
472 return doc->entry->itemid;
476 static void jam_doc_user_changed (JamDoc *doc) {
477 /* if they were editing an old entry or a draft,
478 * that itemid is no longer relevant for the current user. */
479 jam_doc_update_doc(doc);
480 if (doc->entry->itemid != 0) {
481 doc->entry->itemid = 0;
482 g_signal_emit_by_name(doc, "entry_changed");
487 gchar *jam_doc_get_usejournal (JamDoc *doc) {
488 return doc->usejournal;
492 void jam_doc_set_usejournal (JamDoc *doc, const gchar *usejournal) {
493 gchar *uj = usejournal ? g_strdup(usejournal) : NULL;
494 string_replace(&doc->usejournal, uj);
495 jam_doc_user_changed(doc);
496 g_object_notify(G_OBJECT(doc), "usejournal");
500 JamAccount *jam_doc_get_account (JamDoc *doc) {
501 return doc->account;
505 void jam_doc_set_account (JamDoc *doc, JamAccount *acc) {
506 g_object_ref(acc);
507 if (doc->account) g_object_unref(doc->account);
508 doc->account = acc;
509 g_object_notify(G_OBJECT(doc), "account");
510 jam_doc_set_usejournal(doc, NULL);
511 /* set_usejournal calls this for us: jam_doc_user_changed(doc); */
515 const char *jam_doc_get_url (JamDoc *doc) {
516 return doc->url;
520 void jam_doc_set_url (JamDoc *doc, const char *url) {
521 if (doc->url) g_free(doc->url);
522 doc->url = (url ? g_strdup(url) : NULL);
526 void jam_doc_reset_url (JamDoc *doc) {
527 if (doc->url) g_free(doc->url);
528 doc->url = NULL;