You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koffice/lib/kofficecore/kkbdaccessextensions.cpp

668 lines
24 KiB

/* This file is part of the KDE project
Copyright (C) 2005, Gary Cramblitt <garycramblitt@comcast.net>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
// Qt includes
#include <qsplitter.h>
#include <qdockwindow.h>
#include <qdockarea.h>
#include <qevent.h>
#include <qcursor.h>
#include <qobjectlist.h>
#include <qwidgetlist.h>
#include <qlabel.h>
#include <qtooltip.h>
// KDE includes
#include <klocale.h>
#include <kglobal.h>
#include <kapplication.h>
#include <kmainwindow.h>
#include <kaction.h>
#include <kdebug.h>
// KKbdAccessExtensions includes
#include "kkbdaccessextensions.h"
// TODO: See eventFilter method.
//#include "kkbdaccessextensions.moc"
class KPanelKbdSizerIcon : public QCursor
{
public:
KPanelKbdSizerIcon() :
QCursor(Qt::SizeAllCursor),
isActive(false)
{
currentPos = QPoint(-1, -1);
}
~KPanelKbdSizerIcon()
{
hide();
}
void show(const QPoint p) {
if (!isActive) {
originalPos = QCursor::pos();
kapp->setOverrideCursor(*this);
isActive = true;
}
if (p != pos())
setPos(p);
currentPos = p;
}
void hide() {
if (isActive) {
kapp->restoreOverrideCursor();
QCursor::setPos(originalPos);
}
isActive = false;
}
void setShape(int shayp)
{
if (shayp != shape()) {
// Must restore and override to get the icon to refresh.
if (isActive) kapp->restoreOverrideCursor();
QCursor::setShape(shayp);
if (isActive) kapp->setOverrideCursor(*this);
}
}
// Return the difference between a position and where icon is supposed to be.
QSize delta(const QPoint p)
{
QPoint d = p - currentPos;
return QSize(d.x(), d.y());
}
// Return the difference between where the icon is currently positioned and where
// it is supposed to be.
QSize delta() { return delta(pos()); }
// True if the sizing icon is visible.
bool isActive;
private:
// Icon's current position.
QPoint currentPos;
// Mouse cursor's original position when icon is shown.
QPoint originalPos;
};
class KKbdAccessExtensionsPrivate
{
public:
KKbdAccessExtensionsPrivate() :
fwdAction(0),
revAction(0),
accessKeysAction(0),
panel(0),
handleNdx(0),
icon(0),
stepSize(10),
accessKeyLabels(0) {};
~KKbdAccessExtensionsPrivate()
{
delete icon;
// TODO: This crashes, but should delete in the event that KMainWindow is not deleted.
if (accessKeyLabels) {
accessKeyLabels->setAutoDelete(false);
delete accessKeyLabels;
}
}
// Action that starts panel sizing (defaults to F8), forward and reverse;
KAction* fwdAction;
KAction* revAction;
// Action that starts access keys.
KAction* accessKeysAction;
// The splitter or dockwindow currently being sized. If 0, sizing is not in progress.
QWidget* panel;
// Index of current handle of the panel. When panel is a QDockWindow:
// 1 = size horizontally
// 2 = size vertically
uint handleNdx;
// Sizing icon.
KPanelKbdSizerIcon* icon;
// Sizing increment.
int stepSize;
// List of the access key QLabels. If not 0, access keys are onscreen.
QPtrList<QLabel>* accessKeyLabels;
// Pointer to the KMainWindow.
KMainWindow* mainWindow;
};
KKbdAccessExtensions::KKbdAccessExtensions(KMainWindow* parent, const char* name) :
QObject(parent, name)
{
// kdDebug() << "KKbdAccessExtensions::KKbdAccessExtensions: running." << endl;
d = new KKbdAccessExtensionsPrivate;
d->mainWindow = parent;
d->fwdAction = new KAction(i18n("Resize Panel Forward"), KShortcut("F8"),
0, 0, parent->actionCollection(), "resize_panel_forward");
d->revAction = new KAction(i18n("Resize Panel Reverse"), KShortcut("Shift+F8"),
0, 0, parent->actionCollection(), "resize_panel_reverse");
d->accessKeysAction = new KAction(i18n("Access Keys"), KShortcut("Alt+F8"),
0, 0, parent->actionCollection(), "access_keys");
// "Disable" the shortcuts so we can see them in eventFilter.
d->fwdAction->setEnabled(false);
d->revAction->setEnabled(false);
d->accessKeysAction->setEnabled(false);
d->icon = new KPanelKbdSizerIcon();
kapp->installEventFilter(this);
}
KKbdAccessExtensions::~KKbdAccessExtensions()
{
kapp->removeEventFilter(this);
if (d->panel) exitSizing();
delete d;
}
int KKbdAccessExtensions::stepSize() const { return d->stepSize; }
void KKbdAccessExtensions::setStepSize(int s) { d->stepSize = s; }
bool KKbdAccessExtensions::eventFilter( QObject *o, QEvent *e )
{
if ( e->type() == QEvent::KeyPress ) {
// TODO: This permits only a single-key shortcut. For example, Alt+S,R would not work.
// If user configures a multi-key shortcut, it is undefined what will happen here.
// It would be better to handle these as KShortcut activate() signals, but the problem
// is that once a QDockWindow is undocked and has focus, the KShortcut activate() signals
// don't fire anymore.
KShortcut fwdSc = d->fwdAction->shortcut();
KShortcut revSc = d->revAction->shortcut();
KShortcut accessKeysSc = d->accessKeysAction->shortcut();
QKeyEvent* kev = dynamic_cast<QKeyEvent *>(e);
KKey k = KKey(kev);
KShortcut sc = KShortcut(k);
// kdDebug() << "KKbdAccessExtensions::eventFilter: Key press " << sc << endl;
if (!d->accessKeyLabels) {
if (sc == fwdSc) {
nextHandle();
return true;
}
if (sc == revSc) {
prevHandle();
return true;
}
}
if (d->panel) {
if (k == KKey(Key_Escape))
exitSizing();
else
resizePanelFromKey(kev->key(), kev->state());
// Eat the key.
return true;
}
if (sc == accessKeysSc && !d->panel) {
if (d->accessKeyLabels) {
delete d->accessKeyLabels;
d->accessKeyLabels = 0;
} else
displayAccessKeys();
return true;
}
if (d->accessKeyLabels) {
if (k == KKey(Key_Escape)) {
delete d->accessKeyLabels;
d->accessKeyLabels = 0;
} else
handleAccessKey(kev);
return true;
}
return false;
}
else if (d->icon->isActive && e->type() == QEvent::MouseButtonPress) {
exitSizing();
return true;
}
else if (d->accessKeyLabels && e->type() == QEvent::MouseButtonPress) {
delete d->accessKeyLabels;
d->accessKeyLabels = 0;
return true;
}
/* else if (e->type() == QEvent::MouseMove && d->icon->isActive) {
// Lock mouse cursor down.
showIcon();
dynamic_cast<QMouseEvent *>(e)->accept();
return true;
}*/
else if (e->type() == QEvent::MouseMove && d->icon->isActive && d->panel) {
// Resize according to mouse movement.
QMouseEvent* me = dynamic_cast<QMouseEvent *>(e);
QSize s = d->icon->delta();
int dx = s.width();
int dy = s.height();
resizePanel(dx, dy, me->state());
me->accept();
showIcon();
return true;
}
else if (e->type() == QEvent::Resize && d->panel && o == d->panel) {
// TODO: This doesn't always work.
showIcon();
}
return false;
}
QWidgetList* KKbdAccessExtensions::getAllPanels()
{
QWidgetList* allWidgets = kapp->allWidgets();
QWidgetList* allPanels = new QWidgetList;
QWidget* widget = allWidgets->first();
while (widget) {
if (widget->isVisible()) {
if (::qt_cast<QSplitter*>( widget )) {
// Only size QSplitters with at least two handles (there is always one hidden).
if (dynamic_cast<QSplitter *>(widget)->sizes().count() >= 2)
allPanels->append(widget);
} else if (::qt_cast<QDockWindow*>( widget )) {
if (dynamic_cast<QDockWindow *>(widget)->isResizeEnabled()) {
// kdDebug() << "KKbdAccessExtensions::getAllPanels: QDockWindow = " << widget->name() << endl;
allPanels->append(widget);
}
}
}
widget = allWidgets->next();
}
delete allWidgets;
return allPanels;
}
void KKbdAccessExtensions::nextHandle()
{
QWidget* panel = d->panel;
// See if current panel has another handle. If not, find next panel.
if (panel) {
bool advance = true;
d->handleNdx++;
if (::qt_cast<QSplitter*>( panel ))
advance = (d->handleNdx >= dynamic_cast<QSplitter *>(panel)->sizes().count());
else
// Undocked windows have only one "handle" (center).
advance = (d->handleNdx > 2 || !dynamic_cast<QDockWindow *>(panel)->area());
if (advance) {
QWidgetList* allWidgets = getAllPanels();
allWidgets->findRef(panel);
panel = 0;
if (allWidgets->current()) panel = allWidgets->next();
delete allWidgets;
d->handleNdx = 1;
}
} else {
// Find first panel.
QWidgetList* allWidgets = getAllPanels();
panel = allWidgets->first();
delete allWidgets;
d->handleNdx = 1;
}
d->panel = panel;
if (panel)
showIcon();
else
exitSizing();
}
void KKbdAccessExtensions::prevHandle()
{
QWidget* panel = d->panel;
// See if current panel has another handle. If not, find previous panel.
if (panel) {
bool rewind = true;
d->handleNdx--;
rewind = (d->handleNdx < 1);
if (rewind) {
QWidgetList* allWidgets = getAllPanels();
allWidgets->findRef(panel);
panel = 0;
if (allWidgets->current()) panel = allWidgets->prev();
delete allWidgets;
if (panel) {
if (::qt_cast<QSplitter*>( panel ))
d->handleNdx = dynamic_cast<QSplitter *>(panel)->sizes().count() - 1;
else {
if (dynamic_cast<QDockWindow *>(panel)->area())
d->handleNdx = 2;
else
d->handleNdx = 1;
}
}
}
} else {
// Find last panel.
QWidgetList* allWidgets = getAllPanels();
panel = allWidgets->last();
delete allWidgets;
if (panel) {
if (::qt_cast<QSplitter*>( panel ))
d->handleNdx = dynamic_cast<QSplitter *>(panel)->sizes().count() - 1;
else {
if (dynamic_cast<QDockWindow *>(panel)->area())
d->handleNdx = 2;
else
d->handleNdx = 1;
}
}
}
d->panel = panel;
if (panel)
showIcon();
else
exitSizing();
}
void KKbdAccessExtensions::exitSizing()
{
// kdDebug() << "KKbdAccessExtensions::exiting sizing mode." << endl;
hideIcon();
d->handleNdx = 0;
d->panel = 0;
}
void KKbdAccessExtensions::showIcon()
{
if (!d->panel) return;
QPoint p;
// kdDebug() << "KKbdAccessExtensions::showIcon: topLevelWidget = " << d->panel->topLevelWidget()->name() << endl;
if (::qt_cast<QSplitter*>( d->panel )) {
QSplitter* splitter = dynamic_cast<QSplitter *>(d->panel);
int handleNdx = d->handleNdx - 1;
QValueList<int> sizes = splitter->sizes();
// kdDebug() << "KKbdAccessExtensions::showIcon: sizes = " << sizes << endl;
if (splitter->orientation() == Qt::Horizontal) {
d->icon->setShape(Qt::SizeHorCursor);
p.setX(sizes[handleNdx] + (splitter->handleWidth() / 2));
p.setY(splitter->height() / 2);
} else {
d->icon->setShape(Qt::SizeVerCursor);
p.setX(splitter->width() / 2);
p.setY(sizes[handleNdx] + (splitter->handleWidth() / 2));
}
// kdDebug() << "KKbdAccessExtensions::showIcon: p = " << p << endl;
p = splitter->mapToGlobal(p);
// kdDebug() << "KKbdAccessExtensions::showIcon: mapToGlobal = " << p << endl;
} else {
QDockWindow* dockWindow = dynamic_cast<QDockWindow *>(d->panel);
p = dockWindow->pos();
if (dockWindow->area()) {
// kdDebug() << "KKbdAccessExtensions::showIcon: pos = " << p << " of window = " << dockWindow->parentWidget()->name() << endl;
p = dockWindow->parentWidget()->mapTo(dockWindow->topLevelWidget(), p);
// kdDebug() << "KKbdAccessExtensions::showIcon: mapTo = " << p << " of window = " << dockWindow->topLevelWidget()->name() << endl;
// TODO: How to get the handle width?
if (d->handleNdx == 1) {
d->icon->setShape(Qt::SizeHorCursor);
if (dockWindow->area()->orientation() == Qt::Vertical) {
if (dockWindow->area()->handlePosition() == QDockArea::Normal)
// Handle is to the right of the dock window.
p.setX(p.x() + dockWindow->width());
// else Handle is to the left of the dock window.
} else
// Handle is to the right of the dock window.
p.setX(p.x() + dockWindow->width());
p.setY(p.y() + (dockWindow->height() / 2));
} else {
d->icon->setShape(Qt::SizeVerCursor);
p.setX(p.x() + (dockWindow->width() / 2));
if (dockWindow->area()->orientation() == Qt::Vertical)
// Handle is below the dock window.
p.setY(p.y() + dockWindow->height());
else {
if (dockWindow->area()->handlePosition() == QDockArea::Normal)
// Handle is below the dock window.
p.setY(p.y() + dockWindow->height());
// else Handle is above the dock window.
}
}
p = dockWindow->topLevelWidget()->mapToGlobal(p);
} else {
d->icon->setShape(Qt::SizeAllCursor);
p = QPoint(dockWindow->width() / 2, dockWindow->height() / 2);
p = dockWindow->mapToGlobal(p); // Undocked. Position in center of window.
}
}
// kdDebug() << "KKbdAccessExtensions::showIcon: show(p) = " << p << endl;
d->icon->show(p);
}
void KKbdAccessExtensions::hideIcon()
{
d->icon->hide();
}
void KKbdAccessExtensions::resizePanel(int dx, int dy, int state)
{
int adj = dx + dy;
if (adj == 0) return;
// kdDebug() << "KKbdAccessExtensions::resizePanel: panel = " << d->panel->name() << endl;
if (::qt_cast<QSplitter*>( d->panel )) {
QSplitter* splitter = dynamic_cast<QSplitter *>(d->panel);
int handleNdx = d->handleNdx - 1;
QValueList<int> sizes = splitter->sizes();
// kdDebug() << "KKbdAccessExtensions::resizePanel: before sizes = " << sizes << endl;
sizes[handleNdx] = sizes[handleNdx] + adj;
// kdDebug() << "KKbdAccessExtensions::resizePanel: setSizes = " << sizes << endl;
splitter->setSizes(sizes);
QApplication::postEvent(splitter, new QEvent(QEvent::LayoutHint));
} else {
// TODO: How to get the handle width?
QDockWindow* dockWindow = dynamic_cast<QDockWindow *>(d->panel);
if (dockWindow->area()) {
// kdDebug() << "KKbdAccessExtensions::resizePanel: fixedExtent = " << dockWindow->fixedExtent() << endl;
QSize fe = dockWindow->fixedExtent();
if (d->handleNdx == 1) {
// When vertically oriented and dock area is on right side of screen, pressing
// left arrow increases size.
if (dockWindow->area()->orientation() == Qt::Vertical &&
dockWindow->area()->handlePosition() == QDockArea::Reverse) adj = -adj;
int w = fe.width();
if (w < 0) w = dockWindow->width();
w = w + adj;
if (w > 0 ) dockWindow->setFixedExtentWidth(w);
} else {
// When horizontally oriented and dock area is at bottom of screen,
// pressing up arrow increases size.
if (dockWindow->area()->orientation() == Qt::Horizontal &&
dockWindow->area()->handlePosition() == QDockArea::Reverse) adj = -adj;
int h = fe.height();
if (h < 0) h = dockWindow->height();
h = h + adj;
if (h > 0) dockWindow->setFixedExtentHeight(h);
}
dockWindow->updateGeometry();
QApplication::postEvent(dockWindow->area(), new QEvent(QEvent::LayoutHint));
// kdDebug() << "KKbdAccessExtensions::resizePanel: fixedExtent = " << dockWindow->fixedExtent() << endl;
} else {
if (state == Qt::ShiftButton) {
QSize s = dockWindow->size();
s.setWidth(s.width() + dx);
s.setHeight(s.height() + dy);
dockWindow->resize(s);
} else {
QPoint p = dockWindow->pos();
p.setX(p.x() + dx);
p.setY(p.y() + dy);
dockWindow->move(p);
}
}
}
}
void KKbdAccessExtensions::resizePanelFromKey(int key, int state)
{
// kdDebug() << "KPanelKdbSizer::resizePanelFromKey: key = " << key << " state = " << state << endl;
if (!d->panel) return;
int dx = 0;
int dy = 0;
int stepSize = d->stepSize;
switch (key) {
case Qt::Key_Left: dx = -stepSize; break;
case Qt::Key_Right: dx = stepSize; break;
case Qt::Key_Up: dy = -stepSize; break;
case Qt::Key_Down: dy = stepSize; break;
case Qt::Key_Prior: dy = -5 * stepSize; break;
case Qt::Key_Next: dy = 5 * stepSize; break;
}
int adj = dx + dy;
// kdDebug() << "KKbdAccessExtensions::resizePanelFromKey: adj = " << adj << endl;
if (adj != 0)
resizePanel(dx, dy, state);
else {
if (key == Qt::Key_Enter && ::qt_cast<QDockWindow*>( d->panel )) {
QDockWindow* dockWindow = dynamic_cast<QDockWindow *>(d->panel);
if (dockWindow->area())
dockWindow->undock();
else
dockWindow->dock();
}
}
showIcon();
}
void KKbdAccessExtensions::displayAccessKeys()
{
// Build a list of valid access keys that don't collide with shortcuts.
QString availableAccessKeys = "ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890";
QPtrList<KXMLGUIClient> allClients = d->mainWindow->factory()->clients();
QPtrListIterator<KXMLGUIClient> it( allClients );
KXMLGUIClient *client;
while( (client=it.current()) !=0 )
{
++it;
KActionPtrList actions = client->actionCollection()->actions();
for (int j = 0; j < (int)actions.count(); j++) {
KAction* action = actions[j];
KShortcut sc = action->shortcut();
for (int i = 0; i < (int)sc.count(); i++) {
KKeySequence seq = sc.seq(i);
if (seq.count() == 1) {
QString s = seq.toString();
if (availableAccessKeys.contains(s))
availableAccessKeys.remove(s);
}
}
}
}
// Find all visible, focusable widgets and create a QLabel for each. Don't exceed
// available list of access keys.
QWidgetList* allWidgets = kapp->allWidgets();
QWidget* widget = allWidgets->first();
int accessCount = 0;
int maxAccessCount = availableAccessKeys.length();
int overlap = 20;
QPoint prevGlobalPos = QPoint(-overlap, -overlap);
while (widget && (accessCount < maxAccessCount)) {
if (widget->isVisible() && widget->isFocusEnabled() ) {
QRect r = widget->rect();
QPoint p(r.x(), r.y());
// Don't display an access key if within overlap pixels of previous one.
QPoint globalPos = widget->mapToGlobal(p);
QPoint diffPos = globalPos - prevGlobalPos;
if (diffPos.manhattanLength() > overlap) {
accessCount++;
QLabel* lab=new QLabel(widget, "", widget, 0, Qt::WDestructiveClose);
lab->setPalette(QToolTip::palette());
lab->setLineWidth(2);
lab->setFrameStyle(QFrame::Box | QFrame::Plain);
lab->setMargin(3);
lab->adjustSize();
lab->move(p);
if (!d->accessKeyLabels) {
d->accessKeyLabels = new QPtrList<QLabel>;
d->accessKeyLabels->setAutoDelete(true);
}
d->accessKeyLabels->append(lab);
prevGlobalPos = globalPos;
}
}
widget = allWidgets->next();
}
if (accessCount > 0) {
// Sort the access keys from left to right and down the screen.
QValueList<KSortedLabel> sortedLabels;
for (int i = 0; i < accessCount; i++)
sortedLabels.append(KSortedLabel(d->accessKeyLabels->at(i)));
qHeapSort( sortedLabels );
// Assign access key labels.
for (int i = 0; i < accessCount; i++) {
QLabel* lab = sortedLabels[i].label();
QChar s = availableAccessKeys[i];
lab->setText(s);
lab->adjustSize();
lab->show();
}
}
}
// Handling of the HTML accesskey attribute.
bool KKbdAccessExtensions::handleAccessKey( const QKeyEvent* ev )
{
// Qt interprets the keyevent also with the modifiers, and ev->text() matches that,
// but this code must act as if the modifiers weren't pressed
if (!d->accessKeyLabels) return false;
QChar c;
if( ev->key() >= Key_A && ev->key() <= Key_Z )
c = 'A' + ev->key() - Key_A;
else if( ev->key() >= Key_0 && ev->key() <= Key_9 )
c = '0' + ev->key() - Key_0;
else {
// TODO fake XKeyEvent and XLookupString ?
// This below seems to work e.g. for eacute though.
if( ev->text().length() == 1 )
c = ev->text()[ 0 ];
}
if( c.isNull())
return false;
QLabel* lab = d->accessKeyLabels->first();
while (lab) {
if (lab->text() == c) {
lab->buddy()->setFocus();
delete d->accessKeyLabels;
d->accessKeyLabels = 0;
return true;
}
lab = d->accessKeyLabels->next();
}
return false;
}
KSortedLabel::KSortedLabel(QLabel* l) :
m_l(l) { }
KSortedLabel::KSortedLabel() :
m_l(0) { }
bool KSortedLabel::operator<( KSortedLabel l )
{
QPoint p1 = m_l->mapToGlobal(m_l->pos());
QPoint p2 = l.label()->mapToGlobal(l.label()->pos());
return (p1.y() < p2.y() || (p1.y() == p2.y() && p1.x() < p2.x()));
}