1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /**
3  * @file
4  * Inkview - An SVG file viewer.
5  */
6 /*
7  * Authors:
8  *   Tavmjong Bah
9  *
10  * Copyright (C) 2018 Authors
11  *
12  * The contents of this file may be used under the GNU General Public License Version 2 or later.
13  * Read the file 'COPYING' for more information.
14  *
15  */
16 
17 
18 #include "inkview-window.h"
19 
20 #include <iostream>
21 
22 #include "document.h"
23 
24 #include "ui/monitor.h"
25 #include "ui/view/svg-view-widget.h"
26 
27 #include "util/units.h"
28 
InkviewWindow(const Gio::Application::type_vec_files files,bool fullscreen,bool recursive,int timer,double scale,bool preload)29 InkviewWindow::InkviewWindow(const Gio::Application::type_vec_files files,
30                              bool fullscreen,
31                              bool recursive,
32                              int timer,
33                              double scale,
34                              bool preload
35     )
36     : _files(files)
37     , _fullscreen(fullscreen)
38     , _recursive(recursive)
39     , _timer(timer)
40     , _scale(scale)
41     , _preload(preload)
42     , _index(-1)
43     , _view(nullptr)
44     , _controlwindow(nullptr)
45 {
46     _files = create_file_list(_files);
47 
48     if (_preload) {
49         preload_documents();
50     }
51 
52     if (_files.empty()) {
53         throw NoValidFilesException();
54     }
55 
56     _documents.resize( _files.size(), nullptr); // We keep _documents and _files in sync.
57 
58     // Callbacks
59     signal_key_press_event().connect(sigc::mem_fun(*this, &InkviewWindow::key_press), false);
60 
61     if (_timer) {
62         Glib::signal_timeout().connect_seconds(sigc::mem_fun(*this, &InkviewWindow::on_timer), _timer);
63     }
64 
65     // Actions
66     add_action( "show_first", sigc::mem_fun(*this, &InkviewWindow::show_first) );
67     add_action( "show_prev",  sigc::mem_fun(*this, &InkviewWindow::show_prev)  );
68     add_action( "show_next",  sigc::mem_fun(*this, &InkviewWindow::show_next)  );
69     add_action( "show_last",  sigc::mem_fun(*this, &InkviewWindow::show_last)  );
70 
71     // ToDo: Add Pause, Resume.
72 
73     if (_fullscreen) {
74         Gtk::Window::fullscreen();
75     }
76 
77     // Show first file
78     activate_action( "show_first" );
79 }
80 
81 std::vector<Glib::RefPtr<Gio::File> >
create_file_list(const std::vector<Glib::RefPtr<Gio::File>> & files)82 InkviewWindow::create_file_list(const std::vector<Glib::RefPtr<Gio::File > >& files)
83 {
84     std::vector<Glib::RefPtr<Gio::File> > valid_files;
85 
86     static bool first = true;
87 
88     for (auto file : files) {
89         Gio::FileType type = file->query_file_type();
90         switch (type) {
91             case Gio::FILE_TYPE_NOT_KNOWN:
92                 std::cerr << "InkviewWindow: File or directory does not exist: "
93                           << file->get_basename() << std::endl;
94                 break;
95 
96             case Gio::FILE_TYPE_REGULAR:
97             {
98                 // Only look at SVG and SVGZ files.
99                 std::string basename = file->get_basename();
100                 std::string extension = basename.substr(basename.find_last_of(".") + 1);
101                 if (extension == "svg" || extension == "svgz") {
102                     valid_files.push_back(file);
103                 }
104                 break;
105             }
106 
107             case Gio::FILE_TYPE_DIRECTORY:
108             {
109                 if (_recursive || first) {
110                     // No easy way to get children of directory!
111                     Glib::RefPtr<Gio::FileEnumerator> children = file->enumerate_children();
112                     Glib::RefPtr<Gio::FileInfo> info;
113                     std::vector<Glib::RefPtr<Gio::File> > input;
114                     while ((info = children->next_file())) {
115                         input.push_back(children->get_child(info));
116                     }
117                     auto new_files = create_file_list(input);
118                     valid_files.insert(valid_files.end(), new_files.begin(), new_files.end());
119                 }
120                 break;
121             }
122             default:
123                 std::cerr << "InkviewWindow: Unknown file type: " << type << std::endl;
124         }
125         first = false;
126     }
127 
128     return valid_files;
129 }
130 
131 void
update_title()132 InkviewWindow::update_title()
133 {
134     Glib::ustring title(_documents[_index]->getDocumentName());
135 
136     if (_documents.size() > 1) {
137         title += Glib::ustring::compose("  (%1/%2)", _index+1, _documents.size());
138     }
139 
140     set_title(title);
141 }
142 
143 // Returns true if successfully shows document.
144 bool
show_document(SPDocument * document)145 InkviewWindow::show_document(SPDocument* document)
146 {
147     document->ensureUpToDate();  // Crashes on some documents if this isn't called!
148 
149     // Resize window:  (Might be better to use get_monitor_geometry_at_window(this))
150     Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_primary();
151     int width  = MIN((int)document->getWidth().value("px")  * _scale,  monitor_geometry.get_width());
152     int height = MIN((int)document->getHeight().value("px") * _scale,  monitor_geometry.get_height());
153     resize (width, height);
154 
155     if (_view) {
156         _view->setDocument(document);
157     } else {
158         _view = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(document));
159         add (*_view);
160     }
161 
162     update_title();
163 
164     return true;
165 }
166 
167 
168 // Load document, if fail, remove entry from lists.
169 SPDocument*
load_document()170 InkviewWindow::load_document()
171 {
172     SPDocument* document = _documents[_index];
173 
174     if (!document) {
175         // We need to load document. ToDo: Pass Gio::File. Is get_base_name() better?
176         document = SPDocument::createNewDoc (_files[_index]->get_parse_name().c_str(), true, false);
177         if (document) {
178             // We've successfully loaded it!
179             _documents[_index] = document;
180         }
181     }
182 
183     if (!document) {
184         // Failed to load document, remove from vectors.
185         _documents.erase(std::next(_documents.begin(), _index));
186         _files.erase(    std::next(    _files.begin(), _index));
187     }
188 
189     return document;
190 }
191 
192 
193 
194 void
preload_documents()195 InkviewWindow::preload_documents()
196 {
197     for (auto it =_files.begin(); it != _files.end(); ) {
198 
199         SPDocument* document =
200             SPDocument::createNewDoc ((*it)->get_parse_name().c_str(), true, false);
201         if (document) {
202             _documents.push_back(document);
203             ++it;
204         } else {
205             it = _files.erase(it);
206         }
207     }
208 }
209 
210 static std::string window_markup = R"(
211 <interface>
212   <object class="GtkWindow" id="ControlWindow">
213     <child>
214       <object class="GtkButtonBox">
215         <child>
216           <object class="GtkButton" id="show-first">
217             <property name="visible">True</property>
218             <property name="can_focus">True</property>
219             <child>
220               <object class="GtkImage">
221                 <property name="visible">True</property>
222                 <property name="icon_name">go-first</property>
223               </object>
224             </child>
225           </object>
226         </child>
227         <child>
228           <object class="GtkButton" id="show-prev">
229             <property name="visible">True</property>
230             <property name="can_focus">True</property>
231             <child>
232               <object class="GtkImage">
233                 <property name="visible">True</property>
234                 <property name="icon_name">go-previous</property>
235               </object>
236             </child>
237           </object>
238         </child>
239         <child>
240           <object class="GtkButton" id="show-next">
241             <property name="visible">True</property>
242             <property name="can_focus">False</property>
243             <child>
244               <object class="GtkImage">
245                 <property name="visible">True</property>
246                 <property name="icon_name">go-next</property>
247               </object>
248             </child>
249           </object>
250         </child>
251         <child>
252           <object class="GtkButton" id="show-last">
253             <property name="visible">True</property>
254             <property name="can_focus">False</property>
255             <child>
256               <object class="GtkImage">
257                 <property name="visible">True</property>
258                 <property name="icon_name">go-last</property>
259               </object>
260             </child>
261           </object>
262         </child>
263       </object>
264     </child>
265   </object>
266 </interface>
267 )";
268 
269 void
show_control()270 InkviewWindow::show_control()
271 {
272     if (!_controlwindow) {
273 
274         auto builder = Gtk::Builder::create();
275         try
276         {
277             builder->add_from_string(window_markup);
278         }
279         catch (const Glib::Error& err)
280         {
281             std::cerr << "InkviewWindow::show_control: builder failed: " << err.what() << std::endl;
282             return;
283         }
284 
285 
286         builder->get_widget("ControlWindow", _controlwindow);
287         if (!_controlwindow) {
288             std::cerr << "InkviewWindow::show_control: Control Window not found!" << std::endl;
289             return;
290         }
291 
292         // Need to give control window access to viewer window's actions.
293         Glib::RefPtr<Gio::ActionGroup> viewer = get_action_group("win");
294         if (viewer) {
295             _controlwindow->insert_action_group("viewer", viewer);
296         }
297 
298         // Gtk::Button not derived from Gtk::Actionable due to ABI issues. Must use Gtk.
299         // Fixed in Gtk4. In Gtk4 this can be replaced by setting the action in the interface.
300         Gtk::Button* button;
301         builder->get_widget("show-first", button);
302         gtk_actionable_set_action_name( GTK_ACTIONABLE(button->gobj()), "viewer.show_first");
303         builder->get_widget("show-prev", button);
304         gtk_actionable_set_action_name( GTK_ACTIONABLE(button->gobj()), "viewer.show_prev");
305         builder->get_widget("show-next", button);
306         gtk_actionable_set_action_name( GTK_ACTIONABLE(button->gobj()), "viewer.show_next");
307         builder->get_widget("show-last", button);
308         gtk_actionable_set_action_name( GTK_ACTIONABLE(button->gobj()), "viewer.show_last");
309 
310         _controlwindow->set_resizable(false);
311         _controlwindow->set_transient_for(*this);
312         _controlwindow->show_all();
313 
314     } else {
315         _controlwindow->present();
316     }
317 }
318 
319 // Next document
320 void
show_next()321 InkviewWindow::show_next()
322 {
323     ++_index;
324 
325     SPDocument* document = nullptr;
326 
327     while (_index < _documents.size() && !document) {
328         document = load_document();
329     }
330 
331     if (document) {
332         // Show new document
333         show_document(document);
334     } else {
335         // Failed to load new document, keep current.
336         --_index;
337     }
338 }
339 
340 // Previous document
341 void
show_prev()342 InkviewWindow::show_prev()
343 {
344     SPDocument* document = nullptr;
345     int old_index = _index;
346 
347     while (_index > 0 && !document) {
348         --_index;
349         document = load_document();
350     }
351 
352     if (document) {
353         // Show new document
354         show_document(document);
355     } else {
356         // Failed to load new document, keep current.
357         _index = old_index;
358     }
359 }
360 
361 // Show first document
362 void
show_first()363 InkviewWindow::show_first()
364 {
365     _index = -1;
366     show_next();
367 }
368 
369 // Show last document
370 void
show_last()371 InkviewWindow::show_last()
372 {
373     _index = _documents.size();
374     show_prev();
375 }
376 
377 bool
key_press(GdkEventKey * event)378 InkviewWindow::key_press(GdkEventKey* event)
379 {
380     switch (event->keyval) {
381         case GDK_KEY_Up:
382         case GDK_KEY_Home:
383             show_first();
384             break;
385 
386         case GDK_KEY_Down:
387         case GDK_KEY_End:
388             show_last();
389             break;
390 
391         case GDK_KEY_F11:
392             if (_fullscreen) {
393                 unfullscreen();
394                 _fullscreen = false;
395             } else {
396                 fullscreen();
397                 _fullscreen = true;
398             }
399             break;
400 
401         case GDK_KEY_Return:
402             show_control();
403             break;
404 
405         case GDK_KEY_KP_Page_Down:
406         case GDK_KEY_Page_Down:
407         case GDK_KEY_Right:
408         case GDK_KEY_space:
409             show_next();
410             break;
411 
412         case GDK_KEY_KP_Page_Up:
413         case GDK_KEY_Page_Up:
414         case GDK_KEY_Left:
415         case GDK_KEY_BackSpace:
416             show_prev();
417             break;
418 
419         case GDK_KEY_Escape:
420         case GDK_KEY_q:
421         case GDK_KEY_Q:
422             close();
423             break;
424 
425         default:
426             break;
427     }
428     return false;
429 }
430 
431 bool
on_timer()432 InkviewWindow::on_timer()
433 {
434     show_next();
435 
436     // Stop if at end.
437     if (_index >= _documents.size() - 1) {
438         return false;
439     }
440     return true;
441 }
442 
443 /*
444   Local Variables:
445   mode:c++
446   c-file-style:"stroustrup"
447   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
448   indent-tabs-mode:nil
449   fill-column:99
450   End:
451 */
452 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
453