OSDN Git Service

fe8de9d5808e55af3020161a4f6bfb1bf765f415
[fukui-no-namari/dialektos.git] / src / board_window.cxx
1 /*
2  * Copyright (C) 2009 by Aiwota Programmer
3  * aiwotaprog@tetteke.tk
4  *
5  * This file is part of Dialektos.
6  *
7  * Dialektos is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Dialektos is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with Dialektos.  If not, see <http://www.gnu.org/licenses/>.
19  */
20
21 #include "board_window.hxx"
22
23 #include <glibmm/ustring.h>
24 #include <glibmm/refptr.h>
25 #include <sigc++/functors/mem_fun.h>
26 #include <sigc++/signal.h>
27
28 #include <boost/date_time/posix_time/posix_time.hpp>
29 #include <boost/lexical_cast.hpp>
30 #include <boost/filesystem.hpp>
31 #include <boost/format.hpp>
32 #include <boost/bind.hpp>
33 #include <boost/foreach.hpp>
34 #include <boost/mpl/find.hpp>
35
36 #include <string>
37 #include <fstream>
38 #include <iostream>
39 #include <sstream>
40 #include <vector>
41 #include <algorithm>
42
43 #include "bbs_detail_base.hxx"
44 #include "thread_list_model.hxx"
45 #include "board_view_column.hxx"
46 #include "board_subject_item.hxx"
47 #include "thread_idx_cache.hxx"
48 #include "uri_opener.hxx"
49 #include "http_get.hxx"
50 #include "board_subject_idx.hxx"
51 #include "http_date.hxx"
52 #include "misc.hxx"
53 #include "board_window_state.hxx"
54 #include "thread_idx.hxx"
55
56
57 namespace dialektos {
58
59 namespace {
60 struct LessSecond {
61   template <typename PairType>
62   bool operator()(const PairType& lhs, const PairType& rhs) const {
63     return lhs.second < rhs.second;
64   }
65 };
66 }
67
68
69 void BoardWindow::create(std::auto_ptr<bbs_detail::Base> bbs) {
70   regist(new BoardWindow(bbs));
71 }
72
73 BoardWindow::BoardWindow(std::auto_ptr<bbs_detail::Base> bbs) :
74   ApplicationFrameWork(), tree_view_(), bbs_(bbs), scrolled_(),
75   tree_model_(ThreadListModel::create()),
76   http_getter_()
77   {
78
79   // additional menuitems for board window
80   action_group_->add(Gtk::Action::create("FileOpen", "_Open Thread"),
81       sigc::mem_fun(*this, &BoardWindow::on_action_file_open));
82
83   Glib::ustring ui =
84     "<ui>"
85     "  <menubar name='MenuBar'>"
86     "    <menu action='MenuFile'>"
87     "      <menuitem action='FileOpen'/>"
88     "    </menu>"
89     "  </menubar>"
90     "  <popup name='MenuPopup'>"
91     "    <menuitem action='FileOpen'/>"
92     "  </popup>"
93     "</ui>";
94
95   ui_manager_->add_ui_from_string(ui);
96
97
98   tree_view_.signal_row_activated().connect(
99       sigc::mem_fun(*this, &BoardWindow::on_row_activated));
100   tree_view_.signal_button_press_event().connect_notify(
101       sigc::mem_fun(*this, &BoardWindow::on_child_button_press));
102
103
104   scrolled_.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
105   add(scrolled_);
106   scrolled_.add(tree_view_);
107
108   tree_view_.set_model(tree_model_);
109   tree_view_.set_fixed_height_mode(true);
110   tree_view_.set_rules_hint(true);
111
112   BoardWindowState state;
113   state.from_xml(boost::filesystem::path(bbs_->get_board_state_path()));
114
115   add_column<view_column::Number>(state);
116   add_column<view_column::Title>(state);
117   add_column<view_column::ResNum>(state);
118   add_column<view_column::LineCount>(state);
119   add_column<view_column::Average>(state);
120   add_column<view_column::StartTime>(state);
121
122   set_default_size(state.width, state.height);
123   if (state.menubar) menubar_->show();
124   if (state.toolbar) toolbar_->show();
125   if (state.statusbar) statusbar_.show();
126
127   // set columns widths
128   BOOST_FOREACH(Gtk::TreeViewColumn* kolumn, tree_view_.get_columns()) {
129     view_column::Base* column =
130       dynamic_cast<view_column::Base*>(kolumn);
131
132     assert(column);
133     BoardWindowState::ColumnInfo info = state.columns[column->get_id()];
134     if (info.width > 0) column->set_fixed_width(info.width);
135   }
136
137   // set columns order.
138   typedef std::pair<Gtk::TreeViewColumn*, int> PairType;
139   std::vector<PairType> column_order;
140   BOOST_FOREACH(Gtk::TreeViewColumn* kolumn, tree_view_.get_columns()) {
141     view_column::Base* column =
142       dynamic_cast<view_column::Base*>(kolumn);
143
144     assert(column);
145     BoardWindowState::ColumnInfo info = state.columns[column->get_id()];
146     column_order.push_back(std::make_pair(column, info.order));
147   }
148   std::sort(column_order.begin(), column_order.end(), LessSecond());
149
150   Gtk::TreeViewColumn* previous_column = 0;
151   BOOST_FOREACH(PairType& pair, column_order) {
152     if (!previous_column) tree_view_.move_column_to_start(*pair.first);
153     else tree_view_.move_column_after(*pair.first, *previous_column);
154     previous_column = pair.first;
155   }
156
157   using namespace boost::posix_time;
158   ptime start = microsec_clock::local_time();
159
160   load();
161
162   std::cout <<
163   to_simple_string(microsec_clock::local_time() - start) << std::endl;
164
165   set_title(get_uri());
166
167   show_all();
168 }
169
170 BoardWindow::~BoardWindow() {
171 }
172
173 void BoardWindow::on_informed_from(const bbs_detail::Base& bbs,
174     const ThreadIdx& idx) {
175   if (!bbs.belongs_to(*bbs_)) return;
176
177   ModelColumns cols;
178   model_column::field<model_column::Title>(cols) = idx.title_;
179   model_column::field<model_column::ID>(cols) = bbs.get_thread_id();
180   model_column::field<model_column::LineCount>(cols) = idx.line_count_;
181   tree_model_->update_row(cols);
182 }
183
184 void BoardWindow::on_row_activated(const Gtk::TreeModel::Path& path,
185     Gtk::TreeViewColumn* /*col*/) {
186   Gtk::TreeIter iter = tree_model_->get_iter(path);
187   std::string id;
188   iter->get_value(boost::mpl::find<model_column::List,
189       model_column::ID>::type::pos::value, id);
190
191   const std::string uri = bbs_->get_another_thread_uri(id);
192   uri_opener::open(uri);
193 }
194
195 void BoardWindow::on_action_view_stop() {
196   if (http_getter_) http_getter_->cancel();
197 }
198
199 bool BoardWindow::is_same(const bbs_detail::Base& bbs) const {
200   return *bbs_ == bbs;
201 }
202
203 std::string BoardWindow::get_uri() const {
204   return bbs_->get_board_uri();
205 }
206
207
208 void BoardWindow::load() {
209   boost::unordered_map<model_column::ID::type, ModelColumns> buffer;
210
211   std::vector<ThreadIdxCache> caches;
212
213   const boost::filesystem::path dir(bbs_->get_board_idx_dir_path());
214   if (boost::filesystem::exists(dir) && boost::filesystem::is_directory(dir))
215     caches = dialektos::ThreadIdxCache::from_directory(dir);
216
217   BOOST_FOREACH(const ThreadIdxCache& cache, caches) {
218     ModelColumns cols;
219     model_column::field<model_column::Title>(cols) = cache.title_;
220     model_column::field<model_column::ID>(cols) = cache.id_;
221     model_column::field<model_column::LineCount>(cols) = cache.line_count_;
222     buffer[cache.id_] = cols;
223   }
224
225   const boost::filesystem::path sbj(bbs_->get_board_subject_path());
226   if (boost::filesystem::exists(sbj) && boost::filesystem::is_regular(sbj)) {
227     std::vector<SubjectItem> subjects;
228     bbs_->load_subject(subjects);
229
230     idx_ = SubjectIdx::from_xml(
231         boost::filesystem::path(bbs_->get_board_subject_idx_path()));
232
233     merge_subject_txt(subjects, buffer);
234   }
235
236   tree_model_->set_buffer(buffer);
237 }
238
239 void BoardWindow::merge_subject_txt(
240     const std::vector<SubjectItem>& subjects,
241     boost::unordered_map<std::string, ModelColumns>& buffer) {
242
243   boost::posix_time::ptime last_modified;
244   try {
245     last_modified = rfc1123_to_ptime(idx_.last_modified_);
246   } catch (const HTTPDateError& e) {
247     std::cerr << e.what() << std::endl;
248     last_modified = boost::posix_time::second_clock::universal_time();
249   }
250
251   BOOST_FOREACH(const SubjectItem& item, subjects) {
252     ModelColumns& cols = buffer[item.id_];
253     model_column::field<model_column::Number>(cols) = item.number_;
254     model_column::field<model_column::Title>(cols) = item.title_;
255     model_column::field<model_column::ID>(cols) = item.id_;
256     model_column::field<model_column::ResNum>(cols) = item.res_num_;
257
258     try {
259       const std::time_t _start = boost::lexical_cast<std::time_t>(item.id_);
260       const boost::posix_time::ptime start = boost::posix_time::from_time_t(_start);
261       const boost::posix_time::ptime curr = last_modified;
262       const boost::posix_time::time_duration dur = curr - start;
263       const double sec = dur.total_seconds();
264       const double average = item.res_num_ / (sec/60.0/60.0/24.0);
265       model_column::field<model_column::Average>(cols) = average;
266     } catch(const boost::bad_lexical_cast& e) {
267       std::cerr << e.what() << "(" << item.id_ << ")" << std::endl;
268     }
269   }
270 }
271
272 void BoardWindow::unmerge_subject_txt(
273     boost::unordered_map<std::string, ModelColumns>& buffer) {
274   // remove thread ids which line_count_ is zero.
275   std::list<std::string> remove_list;
276   typedef boost::unordered_map<std::string, ModelColumns>::value_type PairType;
277   BOOST_FOREACH(PairType& pair, buffer) {
278     ModelColumns& cols = pair.second;
279     if (model_column::field<model_column::LineCount>(cols) == 0)
280       remove_list.push_back(pair.first);
281     else if (model_column::field<model_column::Number>(cols) != 0) {
282       model_column::field<model_column::Number>(cols) = 0;
283       model_column::field<model_column::ResNum>(cols) = 0;
284       model_column::field<model_column::Average>(cols) = 0;
285     }
286   }
287   BOOST_FOREACH(const std::string& id, remove_list)
288     buffer.erase(id);
289 }
290
291 void BoardWindow::on_action_file_open() {
292   std::cout << "file open activated" << std::endl;
293 }
294
295 void BoardWindow::on_action_edit_copy() {
296   Glib::RefPtr<const Gtk::TreeSelection> selection =
297     tree_view_.get_selection();
298
299   std::vector<Gtk::TreeModel::Path> paths = selection->get_selected_rows();
300   if (paths.empty()) return;
301
302   std::string selected;
303   BOOST_FOREACH(Gtk::TreeModel::Path path, paths) {
304     Gtk::TreeRow row = *(tree_model_->get_iter(path));
305     std::string id;
306     row.get_value(boost::mpl::find<model_column::List,
307         model_column::ID>::type::pos::value, id);
308     if (id.empty()) continue;
309     if (!selected.empty()) selected += "\n";
310     selected += bbs_->get_another_thread_uri(id);
311   }
312   Gtk::Clipboard::get()->set_text(selected);
313 }
314
315 void BoardWindow::on_action_view_refresh() {
316   if (http_getter_) return;
317
318   statusbar_.push("HTTP/1.0 GET...");
319
320   const std::string uri = bbs_->get_board_subject_uri();
321   http::Header request_header = bbs_->get_board_subject_request_header();
322
323   SubjectIdx idx = SubjectIdx::from_xml(
324       boost::filesystem::path(bbs_->get_board_subject_idx_path()));
325   request_header.set_if_modified_since(idx.last_modified_);
326
327   http_getter_.reset(new http::GetInThread(uri, request_header));
328   http_getter_->signal_end().connect(
329       sigc::mem_fun(*this, &BoardWindow::on_http_get_end));
330   http_getter_->run();
331 }
332
333 void BoardWindow::save_content(const http::Response& response) {
334   // save the content to the file subject.txt
335   if (!misc::create_directories(boost::filesystem::path(
336           bbs_->get_board_subject_path()).parent_path())) return;
337   std::ofstream ofs(bbs_->get_board_subject_path().c_str());
338   ofs << response.get_content();
339   ofs.close();
340
341   // save the metadata to the file subject.xml
342   idx_.last_modified_ = response.get_header().get_last_modified();
343   idx_.to_xml(boost::filesystem::path(bbs_->get_board_subject_idx_path()));
344 }
345
346 void BoardWindow::on_http_get_end(bool success) {
347 //  const std::string uri = http_getter_->get_uri();
348 //  const http::Header request_header = http_getter_->get_request_header();
349   const http::Response response = http_getter_->get_response();
350   const boost::system::error_code err = http_getter_->get_error();
351   http_getter_.reset(0);
352   if (err) {
353     statusbar_.push(err.message());
354     return;
355   }
356   if (!success) {
357     statusbar_.push("Canceled.");
358     return;
359   }
360
361   on_refresh_end(response.get_status_line(), response.get_header());
362
363   std::vector<SubjectItem> subjects;
364   bbs_->load_subject_from_string(response.get_content(), subjects);
365
366   if (subjects.empty()) return;
367
368   save_content(response);
369
370   boost::unordered_map<std::string, ModelColumns> buffer;
371   tree_model_->get_buffer(buffer);
372   unmerge_subject_txt(buffer);
373   merge_subject_txt(subjects, buffer);
374   tree_model_->set_buffer(buffer);
375 }
376
377 void BoardWindow::on_refresh_end(const http::StatusLine& status,
378     const http::Header& header) {
379   std::string message = status.get_line();
380   const std::string last_modified = header.get_last_modified();
381   if (!last_modified.empty()) {
382     message += " ";
383     message += last_modified;
384   }
385   statusbar_.push(message);
386 }
387
388 void BoardWindow::save_state() const{
389   BoardWindowState state;
390   state.width = get_width();
391   state.height = get_height();
392   state.menubar = menubar_->is_visible();
393   state.toolbar = toolbar_->is_visible();
394   state.statusbar = statusbar_.is_visible();
395
396   int order = 0;
397   BOOST_FOREACH(const Gtk::TreeViewColumn* kolumn, tree_view_.get_columns()) {
398     const view_column::Base* column =
399       dynamic_cast<const view_column::Base*>(kolumn);
400
401     assert(column);
402
403     BoardWindowState::ColumnInfo info;
404     info.width = column->get_width();
405     info.order = order;
406     state.columns[column->get_id()] = info;
407     ++order;
408
409     if (column->get_sort_indicator()) {
410       state.sort.column = column->get_id();
411       state.sort.ascendant = column->get_sort_order() == Gtk::SORT_ASCENDING;
412     }
413   }
414
415   state.to_xml(boost::filesystem::path(bbs_->get_board_state_path()));
416 }
417
418
419 } // namespace dialektos