1 /*
2     SPDX-FileCopyrightText: 2002 Frerich Raabe <raabe@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "history.h"
8 #include "view.h"
9 #include "khc_debug.h"
10 
11 #include <QAction>
12 #include <QApplication>
13 #include <QIcon>
14 #include <QMenu>
15 #include <QTimer>
16 
17 #include <KActionCollection>
18 #include <KStandardGuiItem>
19 #include <KStringHandler>
20 #include <KToolBarPopupAction>
21 #include <KXMLGUIFactory>
22 #include <KXmlGuiWindow>
23 
24 using namespace KHC;
25 
26 // TODO: Needs complete redo!
27 // TODO: oh yeah
28 
29 History *History::m_instance = nullptr;
30 
self()31 History &History::self()
32 {
33   if  ( !m_instance )
34     m_instance = new History;
35   return *m_instance;
36 }
37 
History()38 History::History() : QObject(),
39   m_goBuffer( 0 )
40 {
41   m_entries_current = m_entries.end();
42 }
43 
~History()44 History::~History()
45 {
46   qDeleteAll(m_entries);
47 }
48 
setupActions(KActionCollection * coll)49 void History::setupActions( KActionCollection *coll )
50 {
51   QPair<KGuiItem, KGuiItem> backForward = KStandardGuiItem::backAndForward();
52 
53   m_backAction = new KToolBarPopupAction( QIcon::fromTheme( backForward.first.iconName() ), backForward.first.text(), this );
54   coll->addAction( QStringLiteral("back"), m_backAction );
55   coll->setDefaultShortcuts(m_backAction, KStandardShortcut::back());
56 
57   connect(m_backAction, &KToolBarPopupAction::triggered, this, &History::back);
58 
59   connect( m_backAction->menu(), &QMenu::triggered, this, &History::backActivated );
60 
61   connect( m_backAction->menu(), &QMenu::aboutToShow, this, &History::fillBackMenu );
62 
63   m_backAction->setEnabled( false );
64 
65   m_forwardAction = new KToolBarPopupAction( QIcon::fromTheme( backForward.second.iconName() ), backForward.second.text(), this );
66   coll->addAction( QStringLiteral("forward"), m_forwardAction );
67   coll->setDefaultShortcuts(m_forwardAction, KStandardShortcut::forward());
68 
69   connect(m_forwardAction, &KToolBarPopupAction::triggered, this, &History::forward);
70 
71   connect( m_forwardAction->menu(), &QMenu::triggered, this, &History::forwardActivated );
72 
73   connect( m_forwardAction->menu(), &QMenu::aboutToShow, this, &History::fillForwardMenu );
74 
75   m_forwardAction->setEnabled( false );
76 }
77 
installMenuBarHook(KXmlGuiWindow * mainWindow)78 void History::installMenuBarHook( KXmlGuiWindow *mainWindow )
79 {
80   QMenu *goMenu = dynamic_cast<QMenu *>(
81       mainWindow->guiFactory()->container( QStringLiteral("go_web"), mainWindow ) );
82   if ( goMenu )
83   {
84     connect(goMenu, &QMenu::aboutToShow, this, &History::fillGoMenu);
85 
86     connect(goMenu, &QMenu::triggered, this, &History::goMenuActivated);
87 
88     m_goMenuIndex = goMenu->actions().count();
89   }
90 }
91 
createEntry()92 void History::createEntry()
93 {
94   qCDebug(KHC_LOG) << "History::createEntry()";
95 
96   // First, remove any forward history
97   if (m_entries_current!=m_entries.end())
98   {
99 
100     m_entries.erase(m_entries.begin(),m_entries_current);
101 
102     // If current entry is empty reuse it.
103     if ( !(*m_entries_current)->view ) {
104       return;
105     }
106   }
107   // Append a new entry
108   m_entries_current = m_entries.insert(m_entries_current, new Entry ); // made current
109 }
110 
updateCurrentEntry(View * view)111 void History::updateCurrentEntry( View *view )
112 {
113   if ( m_entries.isEmpty() )
114     return;
115 
116   QUrl url = view->url();
117 
118   Entry *current = *m_entries_current;
119 
120   QDataStream stream( &current->buffer, QIODevice::WriteOnly );
121   view->browserExtension()->saveState( stream );
122 
123   current->view = view;
124 
125   if ( url.isEmpty() ) {
126     qCDebug(KHC_LOG) << "History::updateCurrentEntry(): internal url";
127     url = view->internalUrl();
128   }
129 
130   qCDebug(KHC_LOG) << "History::updateCurrentEntry(): " << view->title()
131             << " (URL: " << url.url() << ")";
132 
133   current->url = url;
134   current->title = view->title();
135 
136   current->search = view->state() == View::Search;
137 }
138 
updateActions()139 void History::updateActions()
140 {
141   m_backAction->setEnabled( canGoBack() );
142   m_forwardAction->setEnabled( canGoForward() );
143 }
144 
back()145 void History::back()
146 {
147   qCDebug(KHC_LOG) << "History::back()";
148   goHistoryActivated( -1 );
149 }
150 
backActivated(QAction * action)151 void History::backActivated( QAction *action )
152 {
153   int id = action->data().toInt();
154   qCDebug(KHC_LOG) << "History::backActivated(): id = " << id;
155   goHistoryActivated( -( id + 1 ) );
156 }
157 
forward()158 void History::forward()
159 {
160   qCDebug(KHC_LOG) << "History::forward()";
161   goHistoryActivated( 1 );
162 }
163 
forwardActivated(QAction * action)164 void History::forwardActivated( QAction *action )
165 {
166   int id = action->data().toInt();
167   qCDebug(KHC_LOG) << "History::forwardActivated(): id = " << id;
168   goHistoryActivated( id + 1 );
169 }
170 
goHistoryActivated(int steps)171 void History::goHistoryActivated( int steps )
172 {
173   qCDebug(KHC_LOG) << "History::goHistoryActivated(): m_goBuffer = " << m_goBuffer;
174   if ( m_goBuffer )
175     return;
176   m_goBuffer = steps;
177   QTimer::singleShot( 0, this, &History::goHistoryDelayed );
178 }
179 
goHistoryDelayed()180 void History::goHistoryDelayed()
181 {
182   qCDebug(KHC_LOG) << "History::goHistoryDelayed(): m_goBuffer = " << m_goBuffer;
183   if ( !m_goBuffer )
184     return;
185   int steps = m_goBuffer;
186   m_goBuffer = 0;
187   goHistory( steps );
188 }
189 
goHistory(int steps)190 void History::goHistory( int steps )
191 {
192   qCDebug(KHC_LOG) << "History::goHistory(): " << steps;
193 
194   // If current entry is empty remove it.
195   Entry *current = *m_entries_current;
196   if ( current && !current->view ) m_entries_current = m_entries.erase(m_entries_current);
197 
198   EntryList::iterator newPos = m_entries_current - steps;
199 
200   current = *newPos;
201   if ( !current ) {
202     qCWarning(KHC_LOG) << "No History entry at position " << newPos - m_entries.begin();
203     return;
204   }
205 
206   if ( !current->view ) {
207     qCWarning(KHC_LOG) << "Empty history entry." ;
208     return;
209   }
210 
211   m_entries_current = newPos;
212 
213   if ( current->search ) {
214     qCDebug(KHC_LOG) << "History::goHistory(): search";
215     current->view->lastSearch();
216     return;
217   }
218 
219   if ( current->url.scheme() == QLatin1String("khelpcenter") ) {
220     qCDebug(KHC_LOG) << "History::goHistory(): internal";
221     Q_EMIT goInternalUrl( current->url );
222     return;
223   }
224 
225 
226   Q_EMIT goUrl( current->url );
227 
228   Entry h( *current );
229   h.buffer.detach();
230 
231   QDataStream stream( h.buffer );
232 
233   h.view->closeUrl();
234   updateCurrentEntry( h.view );
235   h.view->browserExtension()->restoreState( stream );
236 
237 
238   updateActions();
239 }
240 
fillBackMenu()241 void History::fillBackMenu()
242 {
243   QMenu *menu = m_backAction->menu();
244   menu->clear();
245   fillHistoryPopup( menu, true, false, false );
246 }
247 
fillForwardMenu()248 void History::fillForwardMenu()
249 {
250   QMenu *menu = m_forwardAction->menu();
251   menu->clear();
252   fillHistoryPopup( menu, false, true, false );
253 }
254 
fillGoMenu()255 void History::fillGoMenu()
256 {
257   KXmlGuiWindow *mainWindow = static_cast<KXmlGuiWindow *>( qApp->activeWindow() );
258   QMenu *goMenu = dynamic_cast<QMenu *>( mainWindow->guiFactory()->container( QStringLiteral( "go" ), mainWindow ) );
259   if ( !goMenu || m_goMenuIndex == -1 )
260     return;
261 
262   for ( int i = goMenu->actions().count() - 1 ; i >= m_goMenuIndex; i-- )
263     goMenu->removeAction( goMenu->actions()[i] );
264 
265   // TODO perhaps smarter algorithm (rename existing items, create new ones only if not enough) ?
266 
267   // Ok, we want to show 10 items in all, among which the current url...
268 
269   if ( m_entries.count() <= 9 )
270   {
271     // First case: limited history in both directions -> show it all
272     m_goMenuHistoryStartPos = m_entries.count() - 1; // Start right from the end
273   } else
274     // Second case: big history, in one or both directions
275   {
276     // Assume both directions first (in this case we place the current URL in the middle)
277     m_goMenuHistoryStartPos = (m_entries_current - m_entries.begin()) + 4;
278 
279     // Forward not big enough ?
280     if ( m_goMenuHistoryStartPos > (int) m_entries.count() - 4 )
281       m_goMenuHistoryStartPos = m_entries.count() - 1;
282   }
283   Q_ASSERT( m_goMenuHistoryStartPos >= 0 && (int) m_goMenuHistoryStartPos < m_entries.count() );
284   m_goMenuHistoryCurrentPos = m_entries_current - m_entries.begin(); // for slotActivated
285   fillHistoryPopup( goMenu, false, false, true, m_goMenuHistoryStartPos );
286 }
287 
goMenuActivated(QAction * action)288 void History::goMenuActivated( QAction* action )
289 {
290   KXmlGuiWindow *mainWindow = static_cast<KXmlGuiWindow *>( qApp->activeWindow() );
291   QMenu *goMenu = dynamic_cast<QMenu *>( mainWindow->guiFactory()->container( QStringLiteral( "go" ), mainWindow ) );
292   if ( !goMenu )
293     return;
294 
295   // 1 for first item in the list, etc.
296   int index = goMenu->actions().indexOf(action) - m_goMenuIndex + 1;
297   if ( index > 0 )
298   {
299     qCDebug(KHC_LOG) << "Item clicked has index " << index;
300     // -1 for one step back, 0 for don't move, +1 for one step forward, etc.
301     int steps = ( m_goMenuHistoryStartPos+1 ) - index - m_goMenuHistoryCurrentPos; // make a drawing to understand this :-)
302     qCDebug(KHC_LOG) << "Emit activated with steps = " << steps;
303     goHistory( steps );
304   }
305 }
306 
fillHistoryPopup(QMenu * popup,bool onlyBack,bool onlyForward,bool checkCurrentItem,uint startPos)307 void History::fillHistoryPopup( QMenu *popup, bool onlyBack, bool onlyForward, bool checkCurrentItem, uint startPos )
308 {
309   Q_ASSERT ( popup ); // kill me if this 0... :/
310 
311   Entry * current = *m_entries_current;
312   QList<Entry*>::iterator it = m_entries.begin();
313   if (onlyBack || onlyForward)
314   {
315     it = m_entries_current; // Jump to current item
316     // And move off it
317     if ( !onlyForward ) {
318         if ( it != m_entries.end() ) ++it;
319     } else {
320         if ( it != m_entries.begin() ) --it;
321     }
322   } else if ( startPos )
323     it += startPos; // Jump to specified start pos
324 
325   uint i = 0;
326   while ( it != m_entries.end() )
327   {
328     QString text = (*it)->title;
329     text = KStringHandler::csqueeze(text, 50); //CT: squeeze
330     text.replace( QLatin1Char('&'), QStringLiteral("&&") );
331     QAction *action = popup->addAction( text );
332     action->setData( i );
333     if ( checkCurrentItem && *it == current )
334     {
335       action->setChecked( true ); // no pixmap if checked
336     }
337     if ( ++i > 10 )
338       break;
339     if ( !onlyForward ) {
340         ++it;
341     } else {
342         if ( it == m_entries.begin() ) {
343             it = m_entries.end();
344         } else {
345             --it;
346         }
347     }
348   }
349 }
350 
canGoBack() const351 bool History::canGoBack() const
352 {
353   return m_entries.size()>1 && EntryList::const_iterator(m_entries_current) != (m_entries.begin()+(m_entries.size()-1));
354 }
355 
canGoForward() const356 bool History::canGoForward() const
357 {
358   return EntryList::const_iterator(m_entries_current) != m_entries.constBegin() && m_entries.size() > 1;
359 }
360 
dumpHistory() const361 void History::dumpHistory() const {
362   for(EntryList::const_iterator it = m_entries.constBegin() ; it!=m_entries.constEnd() ; ++it) {
363     qCDebug(KHC_LOG) << (*it)->title << (*it)->url << (it==EntryList::const_iterator(m_entries_current) ? "current" : "" ) ;
364   }
365 
366 }
367 
368 // vim:ts=2:sw=2:et
369