1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2003 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
12 #include "draftstore.h"
17 # include "get_cmd_out.h"
18 # include "smartquotes.h"
40 /* for drafts, the current entry is tracked through entry->itemid.
41 * for files, we need to keep the filename around. */
43 /* we only save as xml, so we need to remember whether they opened
45 gboolean filename_not_xml
;
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
;
60 GtkTextBuffer
*buffer
;
67 gchar
*url
; /* entry URL; valid only after posting */
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",
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",
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
));
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
) {
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
;
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 */
134 GType
jam_doc_get_type (void) {
135 static GType doc_type
= 0;
137 static const GTypeInfo doc_info
= {
148 doc_type
= g_type_register_static(G_TYPE_OBJECT
, "JamDoc", &doc_info
, 0);
154 static void jam_doc_set_property (GObject
*object
, guint prop_id
, const GValue
*value
, GParamSpec
*pspec
) {
159 static void jam_doc_get_property (GObject
*object
, guint prop_id
, GValue
*value
, GParamSpec
*pspec
) {
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;
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
) {
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();
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
);
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
) {
250 entry
= lj_entry_new_from_filename(filename
, type
, &type
, err
);
251 if (!entry
) return FALSE
;
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");
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
);
280 draft_store_free(ds
);
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
);
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
;
296 g_error("jam_doc_save: shouldn't be called without savetarget.\n");
299 jam_doc_set_dirty(doc
, FALSE
);
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. */
308 if (!lj_entry_to_xml_file(entry
, filename
, err
)) {
309 lj_entry_free(entry
);
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
);
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
));
324 if (!save_draft(entry
, acc
, err
)) {
325 lj_entry_free(entry
);
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
);
337 char *jam_doc_get_title (JamDoc
*doc
) {
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
);
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
);
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
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
);
376 gboolean
jam_doc_append_text (JamDoc
*doc
, const char *text
, const char *encoding
) {
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) {
384 if (!g_utf8_validate(text
, strlen(text
), &end
)) return FALSE
;
385 gtk_text_buffer_insert(doc
->buffer
, &pos
, text
, -1);
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);
399 gboolean
jam_doc_insert_file (JamDoc
*doc
, const char *filename
, const char *encoding
, GError
**err
) {
403 if (!g_file_get_contents(filename
, &text
, &len
, err
)) return FALSE
;
405 if (strcmp(encoding
, "UTF-8") == 0) {
407 if (!g_utf8_validate(text
, len
, &end
)) {
408 g_set_error(err
, 0, 0, _("Invalid UTF-8 starting at byte %d."), end
- text
);
412 gtk_text_buffer_insert_at_cursor(doc
->buffer
, text
, -1);
415 newtext
= g_convert(text
, -1, "UTF-8", encoding
, NULL
, NULL
, err
);
420 gtk_text_buffer_insert_at_cursor(doc
->buffer
, newtext
, -1);
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.
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
);
439 gchar
*newtext
= g_locale_to_utf8(output
->str
, output
->len
, NULL
, NULL
, err
);
441 g_string_free(output
, TRUE
);
444 gtk_text_buffer_insert_at_cursor(doc
->buffer
, newtext
, -1);
447 g_string_free(output
, TRUE
);
450 #endif /* HAVE_GTK */
453 int jam_doc_get_flags (JamDoc
*doc
) {
454 LJEntryType type
= jam_doc_get_entry_type(doc
);
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;
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
) {
505 void jam_doc_set_account (JamDoc
*doc
, JamAccount
*acc
) {
507 if (doc
->account
) g_object_unref(doc
->account
);
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
) {
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
);