2 * Copyright (C) 2009 by Aiwota Programmer
3 * aiwotaprog@tetteke.tk
5 * This file is part of Dialektos.
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.
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.
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/>.
21 #include "board_window.hxx"
23 #include <glibmm/ustring.h>
24 #include <glibmm/refptr.h>
25 #include <sigc++/functors/mem_fun.h>
26 #include <sigc++/signal.h>
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>
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"
53 #include "board_window_state.hxx"
54 #include "thread_idx.hxx"
61 template <typename PairType>
62 bool operator()(const PairType& lhs, const PairType& rhs) const {
63 return lhs.second < rhs.second;
69 void BoardWindow::create(std::auto_ptr<bbs_detail::Base> bbs) {
70 regist(new BoardWindow(bbs));
73 BoardWindow::BoardWindow(std::auto_ptr<bbs_detail::Base> bbs) :
74 ApplicationFrameWork(), tree_view_(), bbs_(bbs), scrolled_(),
75 tree_model_(ThreadListModel::create()),
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));
85 " <menubar name='MenuBar'>"
86 " <menu action='MenuFile'>"
87 " <menuitem action='FileOpen'/>"
90 " <popup name='MenuPopup'>"
91 " <menuitem action='FileOpen'/>"
95 ui_manager_->add_ui_from_string(ui);
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));
104 scrolled_.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
106 scrolled_.add(tree_view_);
108 tree_view_.set_model(tree_model_);
109 tree_view_.set_fixed_height_mode(true);
110 tree_view_.set_rules_hint(true);
112 BoardWindowState state;
113 state.from_xml(boost::filesystem::path(bbs_->get_board_state_path()));
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);
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();
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);
133 BoardWindowState::ColumnInfo info = state.columns[column->get_id()];
134 if (info.width > 0) column->set_fixed_width(info.width);
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);
145 BoardWindowState::ColumnInfo info = state.columns[column->get_id()];
146 column_order.push_back(std::make_pair(column, info.order));
148 std::sort(column_order.begin(), column_order.end(), LessSecond());
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;
157 using namespace boost::posix_time;
158 ptime start = microsec_clock::local_time();
163 to_simple_string(microsec_clock::local_time() - start) << std::endl;
165 set_title(get_uri());
170 BoardWindow::~BoardWindow() {
173 void BoardWindow::on_informed_from(const bbs_detail::Base& bbs,
174 const ThreadIdx& idx) {
175 if (!bbs.belongs_to(*bbs_)) return;
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);
184 void BoardWindow::on_row_activated(const Gtk::TreeModel::Path& path,
185 Gtk::TreeViewColumn* /*col*/) {
186 Gtk::TreeIter iter = tree_model_->get_iter(path);
188 iter->get_value(boost::mpl::find<model_column::List,
189 model_column::ID>::type::pos::value, id);
191 const std::string uri = bbs_->get_another_thread_uri(id);
192 uri_opener::open(uri);
195 void BoardWindow::on_action_view_stop() {
196 if (http_getter_) http_getter_->cancel();
199 bool BoardWindow::is_same(const bbs_detail::Base& bbs) const {
203 std::string BoardWindow::get_uri() const {
204 return bbs_->get_board_uri();
208 void BoardWindow::load() {
209 boost::unordered_map<model_column::ID::type, ModelColumns> buffer;
211 std::vector<ThreadIdxCache> caches;
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);
217 BOOST_FOREACH(const ThreadIdxCache& cache, caches) {
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;
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);
230 idx_ = SubjectIdx::from_xml(
231 boost::filesystem::path(bbs_->get_board_subject_idx_path()));
233 merge_subject_txt(subjects, buffer);
235 on_action_view_refresh();
237 tree_model_->set_buffer(buffer);
240 void BoardWindow::merge_subject_txt(
241 const std::vector<SubjectItem>& subjects,
242 boost::unordered_map<std::string, ModelColumns>& buffer) {
244 boost::posix_time::ptime last_modified;
246 if (!idx_.last_modified_.empty())
247 last_modified = rfc1123_to_ptime(idx_.last_modified_);
249 last_modified = boost::posix_time::second_clock::universal_time();
250 } catch (const HTTPDateError& e) {
251 std::cerr << e.what() << std::endl;
252 last_modified = boost::posix_time::second_clock::universal_time();
255 BOOST_FOREACH(const SubjectItem& item, subjects) {
256 ModelColumns& cols = buffer[item.id_];
257 model_column::field<model_column::Number>(cols) = item.number_;
258 model_column::field<model_column::Title>(cols) = item.title_;
259 model_column::field<model_column::ID>(cols) = item.id_;
260 model_column::field<model_column::ResNum>(cols) = item.res_num_;
262 // avoid some threads like '9242006007'
263 if (item.id_.size() == 10 && item.id_[0] == '9') continue;
266 const std::time_t _start = boost::lexical_cast<std::time_t>(item.id_);
267 const boost::posix_time::ptime start = boost::posix_time::from_time_t(_start);
268 const boost::posix_time::ptime curr = last_modified;
269 const boost::posix_time::time_duration dur = curr - start;
270 const double sec = dur.total_seconds();
271 const double average = item.res_num_ / (sec/60.0/60.0/24.0);
272 model_column::field<model_column::Average>(cols) = average;
273 } catch(const boost::bad_lexical_cast& e) {
274 std::cerr << e.what() << "(" << item.id_ << ")" << std::endl;
279 void BoardWindow::unmerge_subject_txt(
280 boost::unordered_map<std::string, ModelColumns>& buffer) {
281 // remove thread ids which line_count_ is zero.
282 std::list<std::string> remove_list;
283 typedef boost::unordered_map<std::string, ModelColumns>::value_type PairType;
284 BOOST_FOREACH(PairType& pair, buffer) {
285 ModelColumns& cols = pair.second;
286 if (model_column::field<model_column::LineCount>(cols) == 0)
287 remove_list.push_back(pair.first);
288 else if (model_column::field<model_column::Number>(cols) != 0) {
289 model_column::field<model_column::Number>(cols) = 0;
290 model_column::field<model_column::ResNum>(cols) = 0;
291 model_column::field<model_column::Average>(cols) = 0;
294 BOOST_FOREACH(const std::string& id, remove_list)
298 void BoardWindow::on_action_file_open() {
299 std::cout << "file open activated" << std::endl;
302 void BoardWindow::on_action_edit_copy() {
303 Glib::RefPtr<const Gtk::TreeSelection> selection =
304 tree_view_.get_selection();
306 std::vector<Gtk::TreeModel::Path> paths = selection->get_selected_rows();
307 if (paths.empty()) return;
309 std::string selected;
310 BOOST_FOREACH(Gtk::TreeModel::Path path, paths) {
311 Gtk::TreeRow row = *(tree_model_->get_iter(path));
313 row.get_value(boost::mpl::find<model_column::List,
314 model_column::ID>::type::pos::value, id);
315 if (id.empty()) continue;
316 if (!selected.empty()) selected += "\n";
317 selected += bbs_->get_another_thread_uri(id);
319 Gtk::Clipboard::get()->set_text(selected);
322 void BoardWindow::on_action_view_refresh() {
323 if (http_getter_) return;
325 statusbar_.push("HTTP/1.0 GET...");
327 const std::string uri = bbs_->get_board_subject_uri();
328 http::Header request_header = bbs_->get_board_subject_request_header();
330 SubjectIdx idx = SubjectIdx::from_xml(
331 boost::filesystem::path(bbs_->get_board_subject_idx_path()));
332 request_header.set_if_modified_since(idx.last_modified_);
334 http_getter_.reset(new http::GetInThread(uri, request_header));
335 http_getter_->signal_end().connect(
336 sigc::mem_fun(*this, &BoardWindow::on_http_get_end));
340 void BoardWindow::save_content(const http::Response& response) {
341 // save the content to the file subject.txt
342 if (!misc::create_directories(boost::filesystem::path(
343 bbs_->get_board_subject_path()).parent_path())) return;
344 std::ofstream ofs(bbs_->get_board_subject_path().c_str());
345 ofs << response.get_content();
348 // save the metadata to the file subject.xml
349 idx_.last_modified_ = response.get_header().get_last_modified();
350 idx_.to_xml(boost::filesystem::path(bbs_->get_board_subject_idx_path()));
353 void BoardWindow::on_http_get_end(bool success) {
354 // const std::string uri = http_getter_->get_uri();
355 // const http::Header request_header = http_getter_->get_request_header();
356 const http::Response response = http_getter_->get_response();
357 const boost::system::error_code err = http_getter_->get_error();
358 http_getter_.reset(0);
360 statusbar_.push(err.message());
364 statusbar_.push("Canceled.");
368 on_refresh_end(response.get_status_line(), response.get_header());
370 std::vector<SubjectItem> subjects;
371 bbs_->load_subject_from_string(response.get_content(), subjects);
373 if (subjects.empty()) return;
375 save_content(response);
377 boost::unordered_map<std::string, ModelColumns> buffer;
378 tree_model_->get_buffer(buffer);
379 unmerge_subject_txt(buffer);
380 merge_subject_txt(subjects, buffer);
381 tree_model_->set_buffer(buffer);
384 void BoardWindow::on_refresh_end(const http::StatusLine& status,
385 const http::Header& header) {
386 std::string message = status.get_line();
387 const std::string last_modified = header.get_last_modified();
388 if (!last_modified.empty()) {
390 message += last_modified;
392 statusbar_.push(message);
395 void BoardWindow::save_state() const{
396 BoardWindowState state;
397 state.width = get_width();
398 state.height = get_height();
399 state.menubar = menubar_->is_visible();
400 state.toolbar = toolbar_->is_visible();
401 state.statusbar = statusbar_.is_visible();
404 BOOST_FOREACH(const Gtk::TreeViewColumn* kolumn, tree_view_.get_columns()) {
405 const view_column::Base* column =
406 dynamic_cast<const view_column::Base*>(kolumn);
410 BoardWindowState::ColumnInfo info;
411 info.width = column->get_width();
413 state.columns[column->get_id()] = info;
416 if (column->get_sort_indicator()) {
417 state.sort.column = column->get_id();
418 state.sort.ascendant = column->get_sort_order() == Gtk::SORT_ASCENDING;
422 state.to_xml(boost::filesystem::path(bbs_->get_board_state_path()));
426 } // namespace dialektos