diff options
Diffstat (limited to 'src/part')
33 files changed, 4639 insertions, 0 deletions
diff --git a/src/part/Config.cpp b/src/part/Config.cpp new file mode 100644 index 0000000..8d2f6b8 --- /dev/null +++ b/src/part/Config.cpp @@ -0,0 +1,62 @@ + +#include "Config.h" +#include <kconfig.h> +#include <kglobal.h> + + +bool Config::scanAcrossMounts; +bool Config::scanRemoteMounts; +bool Config::scanRemovableMedia; +bool Config::varyLabelFontSizes; +bool Config::showSmallFiles; +uint Config::contrast; +uint Config::antiAliasFactor; +uint Config::minFontPitch; +uint Config::defaultRingDepth; +Filelight::MapScheme Config::scheme; +QStringList Config::skipList; + + +inline KConfig& +Filelight::Config::kconfig() +{ + KConfig *config = KGlobal::config(); + config->setGroup( "filelight_part" ); + return *config; +} + +void +Filelight::Config::read() +{ + const KConfig &config = kconfig(); + + scanAcrossMounts = config.readBoolEntry( "scanAcrossMounts", false ); + scanRemoteMounts = config.readBoolEntry( "scanRemoteMounts", false ); + scanRemovableMedia = config.readBoolEntry( "scanRemovableMedia", false ); + varyLabelFontSizes = config.readBoolEntry( "varyLabelFontSizes", true ); + showSmallFiles = config.readBoolEntry( "showSmallFiles", false ); + contrast = config.readNumEntry( "contrast", 75 ); + antiAliasFactor = config.readNumEntry( "antiAliasFactor", 2 ); + minFontPitch = config.readNumEntry( "minFontPitch", QFont().pointSize() - 3); + scheme = (MapScheme) config.readNumEntry( "scheme", 0 ); + skipList = config.readPathListEntry( "skipList" ); + + defaultRingDepth = 4; +} + +void +Filelight::Config::write() +{ + KConfig &config = kconfig(); + + config.writeEntry( "scanAcrossMounts", scanAcrossMounts ); + config.writeEntry( "scanRemoteMounts", scanRemoteMounts ); + config.writeEntry( "scanRemovableMedia", scanRemovableMedia ); + config.writeEntry( "varyLabelFontSizes", varyLabelFontSizes ); + config.writeEntry( "showSmallFiles", showSmallFiles); + config.writeEntry( "contrast", contrast ); + config.writeEntry( "antiAliasFactor", antiAliasFactor ); + config.writeEntry( "minFontPitch", minFontPitch ); + config.writeEntry( "scheme", scheme ); + config.writePathEntry( "skipList", skipList ); +} diff --git a/src/part/Config.h b/src/part/Config.h new file mode 100644 index 0000000..dffaa95 --- /dev/null +++ b/src/part/Config.h @@ -0,0 +1,41 @@ + +#ifndef Config_H +#define Config_H + +#include <qstringlist.h> + +class KConfig; + + +namespace Filelight +{ + enum MapScheme { Rainbow, HighContrast, KDE, FileDensity, ModTime }; + + class Config + { + static KConfig& kconfig(); + + public: + static void read(); + static void write(); + + //keep everything positive, avoid using DON'T, NOT or NO + + static bool scanAcrossMounts; + static bool scanRemoteMounts; + static bool scanRemovableMedia; + static bool varyLabelFontSizes; + static bool showSmallFiles; + static uint contrast; + static uint antiAliasFactor; + static uint minFontPitch; + static uint defaultRingDepth; + + static MapScheme scheme; + static QStringList skipList; + }; +} + +using Filelight::Config; + +#endif diff --git a/src/part/Makefile.am b/src/part/Makefile.am new file mode 100644 index 0000000..2ff41f8 --- /dev/null +++ b/src/part/Makefile.am @@ -0,0 +1,19 @@ +SUBDIRS = radialMap +INCLUDES = $(all_includes) -I$(top_srcdir)/src +METASOURCES = AUTO + +#Part +kde_module_LTLIBRARIES = libfilelight.la +libfilelight_la_LIBADD = ./radialMap/libradialmap.la $(LIB_KFILE) $(LIB_KPARTS) $(LIB_KDEUI) $(LIB_QT) +libfilelight_la_LDFLAGS = $(all_libraries) $(KDE_PLUGIN) +libfilelight_la_SOURCES = \ + dialog.ui \ + part.cpp \ + scan.cpp \ + progressBox.cpp \ + Config.cpp \ + settingsDialog.cpp \ + fileTree.cpp \ + localLister.cpp \ + remoteLister.cpp \ + summaryWidget.cpp diff --git a/src/part/debug.h b/src/part/debug.h new file mode 100644 index 0000000..e5e680b --- /dev/null +++ b/src/part/debug.h @@ -0,0 +1,36 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#ifndef DEBUG_H +#define DEBUG_H + +/** Fancy debug header + * @author Max Howell + * + * Define DEBUG_PREFIX as a string before you include this to insert a fancy debug prefix + * Debug::debug(), can be used as debug() and is just kdDebug() + * use Debug::indent() and Debug::unindent() + */ + +#include <kdebug.h> + +#ifdef NDEBUG +static inline kndbgstream debug() { return kndbgstream(); } +#else +static inline kdbgstream debug() +{ + return kdbgstream( + #ifdef DEBUG_PREFIX + "[" DEBUG_PREFIX "] ", + #endif + 0, 0 ); +} +#endif + +#define error kdError +#define fatal kdFatal +#define warning kdWarning + +#define DEBUG_ANNOUNCE debug() << ">> " << __PRETTY_FUNCTION__ << endl; + +#endif diff --git a/src/part/dialog.ui b/src/part/dialog.ui new file mode 100644 index 0000000..2c1d788 --- /dev/null +++ b/src/part/dialog.ui @@ -0,0 +1,574 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>Dialog</class> +<widget class="QDialog"> + <property name="name"> + <cstring>Dialog</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>415</width> + <height>351</height> + </rect> + </property> + <property name="caption"> + <string>Settings - Filelight</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QTabWidget"> + <property name="name"> + <cstring>tabWidget</cstring> + </property> + <property name="acceptDrops"> + <bool>false</bool> + </property> + <widget class="QWidget"> + <property name="name"> + <cstring>Widget2</cstring> + </property> + <attribute name="title"> + <string>Scannin&g</string> + </attribute> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>4</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Do &not scan these directories:</string> + </property> + <property name="textFormat"> + <enum>PlainText</enum> + </property> + <property name="buddy" stdset="0"> + <cstring>m_listBox</cstring> + </property> + </widget> + <widget class="QListBox"> + <property name="name"> + <cstring>m_listBox</cstring> + </property> + <property name="toolTip" stdset="0"> + <string></string> + </property> + <property name="whatsThis" stdset="0"> + <string>Filelight will not scan these directories unless you specifically request them.</string> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout4</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer> + <property name="name"> + <cstring>spacer2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>180</width> + <height>21</height> + </size> + </property> + </spacer> + <widget class="QPushButton"> + <property name="name"> + <cstring>m_removeButton</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>R&emove</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + <widget class="QPushButton"> + <property name="name"> + <cstring>m_addButton</cstring> + </property> + <property name="text"> + <string>&Add...</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + <property name="default"> + <bool>false</bool> + </property> + </widget> + </hbox> + </widget> + <widget class="Line"> + <property name="name"> + <cstring>line1</cstring> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>15</height> + </size> + </property> + <property name="frameShape"> + <enum>HLine</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="lineWidth"> + <number>1</number> + </property> + <property name="midLineWidth"> + <number>1</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout7</cstring> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer row="1" column="0" rowspan="2" colspan="1"> + <property name="name"> + <cstring>spacer3</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Fixed</enum> + </property> + <property name="sizeHint"> + <size> + <width>16</width> + <height>50</height> + </size> + </property> + </spacer> + <widget class="QCheckBox" row="1" column="1"> + <property name="name"> + <cstring>dontScanRemoteMounts</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>32767</width> + <height>32767</height> + </size> + </property> + <property name="acceptDrops"> + <bool>false</bool> + </property> + <property name="text"> + <string>Exclude remote files&ystems</string> + </property> + <property name="toolTip" stdset="0"> + <string></string> + </property> + <property name="whatsThis" stdset="0"> + <string>Prevents scanning of filesystems that are not on this computer, e.g. NFS or Samba mounts.</string> + </property> + </widget> + <widget class="QCheckBox" row="0" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>scanAcrossMounts</cstring> + </property> + <property name="text"> + <string>Scan across filesystem &boundaries</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Allows scans to enter directories that are part of other filesystems. For example, when unchecked, this will usually prevent the contents of <b>/mnt</b> from being scanned if you scan <b>/</b>.</string> + </property> + </widget> + <widget class="QCheckBox" row="2" column="1"> + <property name="name"> + <cstring>dontScanRemovableMedia</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>E&xclude removable media</string> + </property> + <property name="toolTip" stdset="0"> + <string></string> + </property> + <property name="whatsThis" stdset="0"> + <string>Prevents Filelight from scanning removable media (eg. CD-ROMs).</string> + </property> + </widget> + </grid> + </widget> + </vbox> + </widget> + <widget class="QWidget"> + <property name="name"> + <cstring>Widget3</cstring> + </property> + <attribute name="title"> + <string>&Appearance</string> + </attribute> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QGroupBox"> + <property name="name"> + <cstring>groupBox1</cstring> + </property> + <property name="title"> + <string>Scheme</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QVButtonGroup"> + <property name="name"> + <cstring>colourSchemeGroup</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout10</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel3</cstring> + </property> + <property name="text"> + <string>Co&ntrast</string> + </property> + <property name="textFormat"> + <enum>PlainText</enum> + </property> + <property name="buddy" stdset="0"> + <cstring>contrastSlider</cstring> + </property> + </widget> + <widget class="QSlider"> + <property name="name"> + <cstring>contrastSlider</cstring> + </property> + <property name="minValue"> + <number>1</number> + </property> + <property name="maxValue"> + <number>100</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="whatsThis" stdset="0"> + <string>Here you can vary the contrast of the filemap in realtime.</string> + </property> + </widget> + </hbox> + </widget> + </vbox> + </widget> + <widget class="QCheckBox"> + <property name="name"> + <cstring>useAntialiasing</cstring> + </property> + <property name="text"> + <string>&Use anti-aliasing</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Anti-aliasing the filemap makes it clearer and prettier, unfortunately it also makes rendering very slow.</string> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout10</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="spacing"> + <number>0</number> + </property> + <widget class="QCheckBox"> + <property name="name"> + <cstring>varyLabelFontSizes</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Var&y label font sizes</string> + </property> + <property name="whatsThis" stdset="0"> + <string>The font size of exploded labels can be varied relative to the depth of the directories they represent. This helps you spot the important labels more easily. Set a sensible minimum font size.</string> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout9</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer> + <property name="name"> + <cstring>spacer4</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Fixed</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel2</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Minimum font si&ze:</string> + </property> + <property name="textFormat"> + <enum>PlainText</enum> + </property> + <property name="buddy" stdset="0"> + <cstring>minFontPitch</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>The smallest font size Filelight can use to render labels.</string> + </property> + </widget> + <widget class="KIntSpinBox"> + <property name="name"> + <cstring>minFontPitch</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>3</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maxValue"> + <number>20</number> + </property> + <property name="minValue"> + <number>1</number> + </property> + <property name="value"> + <number>9</number> + </property> + </widget> + </hbox> + </widget> + </vbox> + </widget> + <widget class="QCheckBox"> + <property name="name"> + <cstring>showSmallFiles</cstring> + </property> + <property name="text"> + <string>Show small files</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Some files are too small to be rendered on the filemap. Selecting this option makes these files visible by merging them all into a single "multi-segment".</string> + </property> + </widget> + </vbox> + </widget> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout6</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QPushButton"> + <property name="name"> + <cstring>m_resetButton</cstring> + </property> + <property name="text"> + <string>&Reset</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + <property name="whatsThis" stdset="0"> + <string>Reset any changes you have made since you opened this dialog.</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer1</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>120</width> + <height>30</height> + </size> + </property> + </spacer> + <widget class="QPushButton"> + <property name="name"> + <cstring>m_closeButton</cstring> + </property> + <property name="text"> + <string>&Close</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </hbox> + </widget> + </vbox> +</widget> +<customwidgets> + <customwidget> + <class>QVButtonGroup</class> + <header location="global">qvbuttongroup.h</header> + <sizehint> + <width>-1</width> + <height>-1</height> + </sizehint> + <container>0</container> + <sizepolicy> + <hordata>5</hordata> + <verdata>5</verdata> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + <pixmap>image0</pixmap> + </customwidget> +</customwidgets> +<images> + <image name="image0"> + <data format="PNG" length="824">89504e470d0a1a0a0000000d4948445200000016000000160806000000c4b46c3b000002ff49444154388db59531681c4714863f992dde820cb370815b50600f54e8ca0ba43970712a8fb838438a3895634813d238a5ab80e314ae4d0a812060a4226017c27221c8a9da6b8c4fe0e00d28b0571cec82043b85611f78c12966efa4bb8bc085f29a6567df7cef9f7fdeccaec571cc2cbaddee47ae21e2385e5b9b815f1ebcfcd8de6a63ad25cb338af7c52741acb5a4a729a3d723a82ec6bd99d267bf3f23fc1c4cab2442d14a915986e792fdfa59569766573049417784f1b12e8267954dab24b78714450a28beaf941f847c2a14e70a0841035a2d45d641eb027213c210c69756320767794684d6508bef0befde1a860796e4c402333542b4256c0f0cdd1e50b97191458be6e0e27d81563a87c643d8fb2d7793d685d696413cc8a6cae46f65f7d79c7c62b87b4f2e15fd0fb0d302be0fefde4a0d557a5f35e90f84e0334014d590f855c9de4ecee17e4eb319d1ff3a00ec02f8c67299f283307c61e7d06fbf1782d082588a33e1cf1705fd81cf773f3601e1f9bec59e2f4b5c7ef5209f0ac95f16630cfd818067c103b586dd274a726229cee0fe8380d191cb4d1267d3d58aa1de7d258ceae5d7d0a78fdd269a86f0c52d414c49bbe3762c9b686de41560d7a72e41c4795a6486a78f95e4c4151481d686efbe7b3398ac58b1a23868b8c474aaa8068c8e714a8dd06c1af2a9e5d1c38c641c909dba6e08237f19b358a7ac5cf3479bc2e41f257e55d2ffc6a73833746f09e186cfa387904f2cbffc90a2aa9886d0e99464d3c5965b512cebd01f1800f67672e2a392fb0f023a3d883a053ffddcc2340dd65ab452b6074dc2cd15c1cbceb863daed413e353cdfcfd97d92333a12da6d0181ec3443cf753ef3cdd092de0e116ff1a02cdc157338ca9d7b8269461cfee1ba2139b9286e1a427f10110f2d561555b076d18a39383d4d99a4c0cd0b787f20747b214962c8266e3cdcf0e97c59126ec2f6edd089f40a92f115e0d1eb11ba238461dd6a15f32b53666de841965bb203575a3cc15a48c64a965fe57105e3635db8fa96dcffc431172b5d715d7103dc3fea7f015f373c8ee3b57f0135105a0fae7717960000000049454e44ae426082</data> + </image> +</images> +<connections> + <connection> + <sender>scanAcrossMounts</sender> + <signal>toggled(bool)</signal> + <receiver>Dialog</receiver> + <slot>toggleScanAcrossMounts(bool)</slot> + </connection> + <connection> + <sender>dontScanRemoteMounts</sender> + <signal>toggled(bool)</signal> + <receiver>Dialog</receiver> + <slot>toggleDontScanRemoteMounts(bool)</slot> + </connection> + <connection> + <sender>dontScanRemovableMedia</sender> + <signal>toggled(bool)</signal> + <receiver>Dialog</receiver> + <slot>toggleDontScanRemovableMedia(bool)</slot> + </connection> +</connections> +<tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>colourSchemeGroup</tabstop> + <tabstop>contrastSlider</tabstop> + <tabstop>useAntialiasing</tabstop> + <tabstop>varyLabelFontSizes</tabstop> + <tabstop>minFontPitch</tabstop> + <tabstop>m_resetButton</tabstop> + <tabstop>m_closeButton</tabstop> + <tabstop>m_listBox</tabstop> + <tabstop>m_removeButton</tabstop> + <tabstop>m_addButton</tabstop> + <tabstop>scanAcrossMounts</tabstop> + <tabstop>dontScanRemoteMounts</tabstop> + <tabstop>dontScanRemovableMedia</tabstop> +</tabstops> +<slots> + <slot>toggleDontScanRemovableMedia(bool)</slot> + <slot>toggleDontScanRemoteMounts(bool)</slot> + <slot>toggleScanAcrossMounts(bool)</slot> +</slots> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>qvbuttongroup.h</includehint> + <includehint>knuminput.h</includehint> +</includehints> +</UI> diff --git a/src/part/fileTree.cpp b/src/part/fileTree.cpp new file mode 100644 index 0000000..f459d50 --- /dev/null +++ b/src/part/fileTree.cpp @@ -0,0 +1,67 @@ +//Author: Max Howell <[email protected]>, (C) 2004 +//Copyright: See COPYING file that comes with this distribution + +#include "fileTree.h" +#include <kglobal.h> +#include <klocale.h> +#include <qfile.h> + + +//static definitions +const uint File::DENOMINATOR[4] = { 1<<0, 1<<10, 1<<20, 1<<30 }; +static const char PREFIX[4] = { 'K', 'M', 'G', 'T' }; + + +QString +File::fullPath( const Directory *root /*= 0*/ ) const +{ + QString path; + + if( root == this ) + root = 0; //prevent returning empty string when there is something we could return + + for( const Directory *d = (Directory*)this; d != root && d; d = d->parent() ) + path.prepend( d->name() ); + + return path; +} + +QString +File::humanReadableSize( UnitPrefix key /*= mega*/ ) const //FIXME inline +{ + return humanReadableSize( m_size, key ); +} + +QString +File::humanReadableSize( uint size, UnitPrefix key /*= mega*/ ) //static +{ + if( size == 0 ) + return "0 B"; + + QString s; + double prettySize = (double)size / (double)DENOMINATOR[key]; + const KLocale &locale = *KGlobal::locale(); + + if( prettySize >= 0.01 ) + { + //use three significant figures + if( prettySize < 1 ) s = locale.formatNumber( prettySize, 2 ); + else if( prettySize < 100 ) s = locale.formatNumber( prettySize, 1 ); + else s = locale.formatNumber( prettySize, 0 ); + + s += ' '; + s += PREFIX[key]; + s += 'B'; + } + + if( prettySize < 0.1 ) + { + s += " ("; + s += locale.formatNumber( size / DENOMINATOR[key - 1], 0 ); + s += ' '; + s += PREFIX[key - 1]; + s += "B)"; + } + + return s; +} diff --git a/src/part/fileTree.h b/src/part/fileTree.h new file mode 100644 index 0000000..25ac6fb --- /dev/null +++ b/src/part/fileTree.h @@ -0,0 +1,238 @@ +//Author: Max Howell <[email protected]>, (C) 2004 +//Copyright: See COPYING file that comes with this distribution + +#ifndef FILETREE_H +#define FILETREE_H + +#include <qcstring.h> //qstrdup +#include <qfile.h> //decodeName() +#include <stdlib.h> + + +//TODO these are pointlessly general purpose now, make them incredibly specific + + + +typedef unsigned long int FileSize; +typedef unsigned long int Dirsize; //**** currently unused + +template <class T> class Iterator; +template <class T> class ConstIterator; +template <class T> class Chain; + +template <class T> +class Link +{ +public: + Link( T* const t ) : prev( this ), next( this ), data( t ) {} + Link() : prev( this ), next( this ), data( 0 ) {} + +//TODO unlinking is slow and you don't use it very much in this context. +// ** Perhaps you can make a faster deletion system that doesn't bother tidying up first +// ** and then you MUST call some kind of detach() function when you remove elements otherwise + ~Link() { delete data; unlink(); } + + friend class Iterator<T>; + friend class ConstIterator<T>; + friend class Chain<T>; + +private: + void unlink() { prev->next = next; next->prev = prev; prev = next = this; } + + Link<T>* prev; + Link<T>* next; + + T* data; //ensure only iterators have access to this +}; + + +template <class T> +class Iterator +{ +public: + Iterator() : link( 0 ) { } //**** remove this, remove this REMOVE THIS!!! dangerous as your implementation doesn't test for null links, always assumes they can be derefenced + Iterator( Link<T> *p ) : link( p ) { } + + bool operator==( const Iterator<T>& it ) const { return link == it.link; } + bool operator!=( const Iterator<T>& it ) const { return link != it.link; } + bool operator!=( const Link<T> *p ) const { return p != link; } + + //here we have a choice, really I should make two classes one const the other not + const T* operator*() const { return link->data; } + T* operator*() { return link->data; } + + Iterator<T>& operator++() { link = link->next; return *this; } //**** does it waste time returning in places where we don't use the retval? + + bool isNull() const { return (link == 0); } //REMOVE WITH ABOVE REMOVAL you don't want null iterators to be possible + + void transferTo( Chain<T> &chain ) + { + chain.append( remove() ); + } + + T* const remove() //remove from list, delete Link, data is returned NOT deleted + { + T* const d = link->data; + Link<T>* const p = link->prev; + + link->data = 0; + delete link; + link = p; //make iterator point to previous element, YOU must check this points to an element + + return d; + } + +private: + Link<T> *link; +}; + + +template <class T> +class ConstIterator +{ +public: + ConstIterator( Link<T> *p ) : link( p ) { } + + bool operator==( const Iterator<T>& it ) const { return link == it.link; } + bool operator!=( const Iterator<T>& it ) const { return link != it.link; } + bool operator!=( const Link<T> *p ) const { return p != link; } + + const T* operator*() const { return link->data; } + + ConstIterator<T>& operator++() { link = link->next; return *this; } + +private: + const Link<T> *link; +}; + + +template <class T> +class Chain +{ +public: + virtual ~Chain() { empty(); } + + void append( T* const data ) + { + Link<T>* const link = new Link<T>( data ); + + link->prev = head.prev; + link->next = &head; + + head.prev->next = link; + head.prev = link; + } + + void transferTo( Chain &c ) + { + if( isEmpty() ) return; + + Link<T>* const first = head.next; + Link<T>* const last = head.prev; + + head.unlink(); + + first->prev = c.head.prev; + c.head.prev->next = first; + + last->next = &c.head; + c.head.prev = last; + } + + void empty() { while( head.next != &head ) { delete head.next; } } + + Iterator<T> iterator() const { return Iterator<T>( head.next ); } + ConstIterator<T> constIterator() const { return ConstIterator<T>( head.next ); } + const Link<T> *end() const { return &head; } + bool isEmpty() const { return head.next == &head; } + +private: + Link<T> head; + void operator=( const Chain& ); +}; + + +class Directory; +class QString; + +class File +{ +public: + friend class Directory; + + enum UnitPrefix { kilo, mega, giga, tera }; + + static const uint DENOMINATOR[4]; + +public: + File( const char *name, FileSize size ) : m_parent( 0 ), m_name( qstrdup( name ) ), m_size( size ) {} + virtual ~File() { delete [] m_name; } + + const Directory *parent() const { return m_parent; } + const char *name8Bit() const { return m_name; } + const FileSize size() const { return m_size; } + QString name() const { return QFile::decodeName( m_name ); } + + virtual bool isDirectory() const { return false; } + + QString fullPath( const Directory* = 0 ) const; + QString humanReadableSize( UnitPrefix key = mega ) const; + +public: + static QString humanReadableSize( uint size, UnitPrefix Key = mega ); + +protected: + File( const char *name, FileSize size, Directory *parent ) : m_parent( parent ), m_name( qstrdup( name ) ), m_size( size ) {} + + Directory *m_parent; //0 if this is treeRoot + char *m_name; + FileSize m_size; //in units of KiB + +private: + File( const File& ); + void operator=( const File& ); +}; + + +class Directory : public Chain<File>, public File +{ +public: + Directory( const char *name ) : File( name, 0 ), m_children( 0 ) {} //DON'T pass the full path! + + uint children() const { return m_children; } + virtual bool isDirectory() const { return true; } + + ///appends a Directory + void append( Directory *d, const char *name=0 ) + { + if( name ) { + delete [] d->m_name; + d->m_name = qstrdup( name ); } //directories that had a fullpath copy just their names this way + + m_children += d->children(); //doesn't include the dir itself + d->m_parent = this; + append( (File*)d ); //will add 1 to filecount for the dir itself + } + + ///appends a File + void append( const char *name, FileSize size ) + { + append( new File( name, size, this ) ); + } + +private: + void append( File *p ) + { + m_children++; + m_size += p->size(); + Chain<File>::append( p ); + } + + uint m_children; + +private: + Directory( const Directory& ); //undefined + void operator=( const Directory& ); //undefined +}; + +#endif diff --git a/src/part/localLister.cpp b/src/part/localLister.cpp new file mode 100644 index 0000000..6bf7945 --- /dev/null +++ b/src/part/localLister.cpp @@ -0,0 +1,333 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include "Config.h" +#include "debug.h" +#include <dirent.h> +#include "fileTree.h" +#include <fstab.h> +#include "localLister.h" +#ifdef HAVE_MNTENT_H +#include <mntent.h> +#endif +#include <qapplication.h> //postEvent() +#include <qfile.h> +#include "scan.h" +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +namespace Filelight +{ + QStringList LocalLister::s_remoteMounts; + QStringList LocalLister::s_localMounts; + + LocalLister::LocalLister( const QString &path, Chain<Directory> *cachedTrees, QObject *parent ) + : QThread() + , m_path( path ) + , m_trees( cachedTrees ) + , m_parent( parent ) + { + //add empty directories for any mount points that are in the path + //TODO empty directories is not ideal as adds to fileCount incorrectly + + QStringList list( Config::skipList ); + if( !Config::scanAcrossMounts ) list += s_localMounts; + if( !Config::scanRemoteMounts ) list += s_remoteMounts; + + for( QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it ) + if( (*it).startsWith( path ) ) + //prevent scanning of these directories + m_trees->append( new Directory( (*it).local8Bit() ) ); + + start(); + } + + void + LocalLister::run() + { + //recursively scan the requested path + const QCString path = QFile::encodeName( m_path ); + Directory *tree = scan( path, path ); + + //delete the list of trees useful for this scan, + //in a sucessful scan the contents would now be transfered to 'tree' + delete m_trees; + + if( ScanManager::s_abort ) //scan was cancelled + { + debug() << "Scan succesfully aborted\n"; + delete tree; + tree = 0; + } + + QCustomEvent *e = new QCustomEvent( 1000 ); + e->setData( tree ); + QApplication::postEvent( m_parent, e ); + } + + // from system.h in GNU coreutils package + /* Extract or fake data from a `struct stat'. + ST_BLKSIZE: Preferred I/O blocksize for the file, in bytes. + ST_NBLOCKS: Number of blocks in the file, including indirect blocks. + ST_NBLOCKSIZE: Size of blocks used when calculating ST_NBLOCKS. */ + #ifndef HAVE_STRUCT_STAT_ST_BLOCKS + #define ST_BLKSIZE(statbuf) DEV_BSIZE + #if defined _POSIX_SOURCE || !defined BSIZE /* fileblocks.c uses BSIZE. */ + #define ST_NBLOCKS(statbuf) ((statbuf).st_size / ST_NBLOCKSIZE + ((statbuf).st_size % ST_NBLOCKSIZE != 0)) + #else /* !_POSIX_SOURCE && BSIZE */ + #define ST_NBLOCKS(statbuf) (S_ISREG ((statbuf).st_mode) || S_ISDIR ((statbuf).st_mode) ? st_blocks ((statbuf).st_size) : 0) + #endif /* !_POSIX_SOURCE && BSIZE */ + #else /* HAVE_STRUCT_STAT_ST_BLOCKS */ + /* Some systems, like Sequents, return st_blksize of 0 on pipes. + Also, when running `rsh hpux11-system cat any-file', cat would + determine that the output stream had an st_blksize of 2147421096. + So here we arbitrarily limit the `optimal' block size to 4MB. + If anyone knows of a system for which the legitimate value for + st_blksize can exceed 4MB, please report it as a bug in this code. */ + #define ST_BLKSIZE(statbuf) ((0 < (statbuf).st_blksize && (statbuf).st_blksize <= (1 << 22)) /* 4MiB */ ? (statbuf).st_blksize : DEV_BSIZE) + #if defined hpux || defined __hpux__ || defined __hpux + /* HP-UX counts st_blocks in 1024-byte units. + This loses when mixing HP-UX and BSD filesystems with NFS. */ + #define ST_NBLOCKSIZE 1024 + #else /* !hpux */ + #if defined _AIX && defined _I386 + /* AIX PS/2 counts st_blocks in 4K units. */ + #define ST_NBLOCKSIZE (4 * 1024) + #else /* not AIX PS/2 */ + #if defined _CRAY + #define ST_NBLOCKS(statbuf) (S_ISREG ((statbuf).st_mode) || S_ISDIR ((statbuf).st_mode) ? (statbuf).st_blocks * ST_BLKSIZE(statbuf)/ST_NBLOCKSIZE : 0) + #endif /* _CRAY */ + #endif /* not AIX PS/2 */ + #endif /* !hpux */ + #endif /* HAVE_STRUCT_STAT_ST_BLOCKS */ + + #ifndef ST_NBLOCKS + #define ST_NBLOCKS(statbuf) ((statbuf).st_blocks) + #endif + + #ifndef ST_NBLOCKSIZE + #define ST_NBLOCKSIZE 512 + #endif + +//some GNU systems don't support big files for some reason +#ifdef __USE_LARGEFILE64 //see dirent.h + #define dirent dirent64 + #define scandir scandir64 + #define stat stat64 + #define statstruct stat64 + #define lstat lstat64 + #define readdir readdir64 +#endif + +#ifndef NULL +#define NULL 0 +#endif + + + #include <errno.h> + static void + outputError( QCString path ) + { + ///show error message that stat or opendir may give + + #define out( s ) error() << s ": " << path << endl; break + + switch( errno ) { + case EACCES: + out( "Inadequate access permisions" ); + case EMFILE: + out( "Too many file descriptors in use by Filelight" ); + case ENFILE: + out( "Too many files are currently open in the system" ); + case ENOENT: + out( "A component of the path does not exist, or the path is an empty string" ); + case ENOMEM: + out( "Insufficient memory to complete the operation" ); + case ENOTDIR: + out( "A component of the path is not a directory" ); + case EBADF: + out( "Bad file descriptor" ); + case EFAULT: + out( "Bad address" ); + case ELOOP: //NOTE shouldn't ever happen + out( "Too many symbolic links encountered while traversing the path" ); + case ENAMETOOLONG: + out( "File name too long" ); + } + + #undef out + } + + Directory* + LocalLister::scan( const QCString &path, const QCString &dirname ) + { + Directory *cwd = new Directory( dirname ); + DIR *dir = opendir( path ); + + if( !dir ) { + outputError( path ); + return cwd; + } + + struct stat statbuf; + dirent *ent; + while ((ent = readdir( dir ))) + { + if( ScanManager::s_abort ) + return cwd; + + if( qstrcmp( ent->d_name, "." ) == 0 || qstrcmp( ent->d_name, ".." ) == 0 ) + continue; + + QCString new_path = path; new_path += ent->d_name; + + //get file information + if( lstat( new_path, &statbuf ) == -1 ) { + outputError( new_path ); + continue; + } + + if( S_ISLNK( statbuf.st_mode ) || + S_ISCHR( statbuf.st_mode ) || + S_ISBLK( statbuf.st_mode ) || + S_ISFIFO( statbuf.st_mode ) || + S_ISSOCK( statbuf.st_mode ) ) + { + continue; + } + + if( S_ISREG( statbuf.st_mode ) ) //file + //using units of KiB as 32bit max is 4GiB and 64bit ints are expensive + cwd->append( ent->d_name, (ST_NBLOCKS( statbuf ) * ST_NBLOCKSIZE) / 1024 ); + + else if( S_ISDIR( statbuf.st_mode ) ) //directory + { + Directory *d = 0; + QCString new_dirname = ent->d_name; + new_dirname += '/'; + new_path += '/'; + + //check to see if we've scanned this section already + + for( Iterator<Directory> it = m_trees->iterator(); it != m_trees->end(); ++it ) + { + if( new_path == (*it)->name8Bit() ) + { + debug() << "Tree pre-completed: " << (*it)->name() << "\n"; + d = it.remove(); + ScanManager::s_files += d->children(); + //**** ideally don't have this redundant extra somehow + cwd->append( d, new_dirname ); + } + } + + if( !d ) //then scan + if ((d = scan( new_path, new_dirname ))) //then scan was successful + cwd->append( d ); + } + + ++ScanManager::s_files; + } + + closedir( dir ); + + return cwd; + } + + bool + LocalLister::readMounts() + { + #define INFO_PARTITIONS "/proc/partitions" + #define INFO_MOUNTED_PARTITIONS "/etc/mtab" /* on Linux... */ + + //**** SHAMBLES + // ** mtab should have priority as mount points don't have to follow fstab + // ** no removable media detection + // ** no updates if mounts change + // ** you want a KDE extension that handles this for you really + + struct fstab *fstab_ent; +#ifdef HAVE_MNTENT_H + struct mntent *mnt_ent; +#endif + QString str; + + +#ifdef HAVE_MNTENT_H + FILE *fp; + if( setfsent() == 0 || !( fp = setmntent( INFO_MOUNTED_PARTITIONS, "r" ) ) ) +#else + if( setfsent() == 0 ) +#endif + return false; + + #define FS_NAME fstab_ent->fs_spec // device-name + #define FS_FILE fstab_ent->fs_file // mount-point + #define FS_TYPE fstab_ent->fs_vfstype // fs-type + #define FS_MNTOPS fstab_ent->fs_mntops // mount-options + + QStringList remoteFsTypes; + remoteFsTypes << "smbfs" ; +#ifdef MNTTYPE_NFS + remoteFsTypes << MNTTYPE_NFS; +#else + remoteFsTypes << "nfs"; +#endif + // What about afs? + + while( (fstab_ent = getfsent()) != NULL ) + { + str = QString( FS_FILE ); + if( str == "/" ) continue; + str += '/'; + + if( remoteFsTypes.contains( FS_TYPE ) ) + s_remoteMounts.append( str ); //**** NO! can't be sure won't have trailing slash, need to do a check first dummy!! + + else + s_localMounts.append( str ); //**** NO! can't be sure won't have trailing slash, need to do a check first dummy!! + + kdDebug() << "FSTAB: " << FS_TYPE << "\n"; + } + + endfsent(); /* close fstab.. */ + + #undef FS_NAME + #undef FS_FILE + #undef FS_TYPE + #undef FS_MNTOPS + + #define FS_NAME mnt_ent->mnt_fsname // device-name + #define FS_FILE mnt_ent->mnt_dir // mount-point + #define FS_TYPE mnt_ent->mnt_type // fs-type + #define FS_MNTOPS mnt_ent->mnt_opts // mount-options + + //scan mtab, **** mtab should take priority, but currently it isn't + +#ifdef HAVE_MNTENT_H + while( ( mnt_ent = getmntent( fp ) ) != NULL ) + { + bool b = false; + + str = QString( FS_FILE ); + if( str == "/" ) continue; + str += "/"; + + if( remoteFsTypes.contains( FS_TYPE ) ) + if( b = !s_remoteMounts.contains( str ) ) + s_remoteMounts.append( str ); //**** NO! can't be sure won't have trailing slash, need to do a check first dummy!! + + else if( b = !s_localMounts.contains( str ) ) + s_localMounts.append( str ); //**** NO! can't be sure won't have trailing slash, need to do a check first dummy!! + + if( b ) kdDebug() << "MTAB: " << FS_TYPE << "\n"; + } + + endmntent( fp ); /* close mtab.. */ +#endif + + + return true; + } +} diff --git a/src/part/localLister.h b/src/part/localLister.h new file mode 100644 index 0000000..5070e14 --- /dev/null +++ b/src/part/localLister.h @@ -0,0 +1,35 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#ifndef LOCALLISTER_H +#define LOCALLISTER_H + +#include <qthread.h> + +class Directory; +template<class T> class Chain; + +namespace Filelight +{ + class LocalLister : public QThread + { + public: + LocalLister( const QString &path, Chain<Directory> *cachedTrees, QObject *parent ); + + static bool readMounts(); + + private: + QString m_path; + Chain<Directory> *m_trees; + QObject *m_parent; + + private: + virtual void run(); + Directory *scan( const QCString&, const QCString& ); + + private: + static QStringList s_localMounts, s_remoteMounts; //TODO namespace + }; +} + +#endif diff --git a/src/part/part.cpp b/src/part/part.cpp new file mode 100644 index 0000000..9a4742a --- /dev/null +++ b/src/part/part.cpp @@ -0,0 +1,254 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include "Config.h" +#include "debug.h" +#include "define.h" +#include "fileTree.h" +#include "part.h" +#include "progressBox.h" +#include "radialMap/widget.h" +#include "scan.h" +#include "settingsDialog.h" +#include "summaryWidget.h" + +#include <kaboutdata.h> //::createAboutData() +#include <kaction.h> +#include <klocale.h> +#include <kmessagebox.h> //::start() +//#include <konq_operations.h> +#include <kparts/genericfactory.h> +#include <kstatusbar.h> +#include <kstdaction.h> +#include <qfile.h> //encodeName() +#include <qtimer.h> //postInit() hack +#include <qvbox.h> +#include <unistd.h> //access() + + +namespace Filelight { + + +typedef KParts::GenericFactory<Filelight::Part> Factory; +K_EXPORT_COMPONENT_FACTORY( libfilelight, Filelight::Factory ) + + +BrowserExtension::BrowserExtension( Part *parent, const char *name ) + : KParts::BrowserExtension( parent, name ) +{} + + +Part::Part( QWidget *parentWidget, const char *widgetName, QObject *parent, const char *name, const QStringList& ) + : ReadOnlyPart( parent, name ) + , m_ext( new BrowserExtension( this ) ) + , m_statusbar( new StatusBarExtension( this ) ) + , m_map( 0 ) + , m_manager( new ScanManager( this ) ) + , m_started( false ) +{ + QPixmap::setDefaultOptimization( QPixmap::BestOptim ); + + Config::read(); + + setInstance( Factory::instance() ); + setWidget( new QVBox( parentWidget, widgetName ) ); + setXMLFile( "filelight_partui.rc" ); + + m_map = new RadialMap::Widget( widget() ); + m_map->hide(); + + KStdAction::zoomIn( m_map, SLOT(zoomIn()), actionCollection() ); + KStdAction::zoomOut( m_map, SLOT(zoomOut()), actionCollection() ); + KStdAction::preferences( this, SLOT(configFilelight()), actionCollection(), "configure_filelight" )->setText( i18n( "Configure Filelight..." ) ); + + connect( m_map, SIGNAL(created( const Directory* )), SIGNAL(completed()) ); + connect( m_map, SIGNAL(created( const Directory* )), SLOT(mapChanged( const Directory* )) ); + connect( m_map, SIGNAL(activated( const KURL& )), SLOT(updateURL( const KURL& )) ); + + // TODO make better system + connect( m_map, SIGNAL(giveMeTreeFor( const KURL& )), SLOT(updateURL( const KURL& )) ); + connect( m_map, SIGNAL(giveMeTreeFor( const KURL& )), SLOT(openURL( const KURL& )) ); + + connect( m_manager, SIGNAL(completed( Directory* )), SLOT(scanCompleted( Directory* )) ); + connect( m_manager, SIGNAL(aboutToEmptyCache()), m_map, SLOT(invalidate()) ); + + QTimer::singleShot( 0, this, SLOT(postInit()) ); +} + +void +Part::postInit() +{ + if( m_url.isEmpty() ) //if url is not empty openURL() has been called immediately after ctor, which happens + { + QWidget *summary = new SummaryWidget( widget(), "summaryWidget" ); + connect( summary, SIGNAL(activated( const KURL& )), SLOT(openURL( const KURL& )) ); + summary->show(); + + //FIXME KXMLGUI is b0rked, it should allow us to set this + //BEFORE createGUI is called but it doesn't + stateChanged( "scan_failed" ); + } +} + +bool +Part::openURL( const KURL &u ) +{ + //we don't want to be using the summary screen anymore + delete widget()->child( "summaryWidget" ); + m_map->show(); + + //TODO everyone hates dialogs, instead render the text in big fonts on the Map + //TODO should have an empty KURL until scan is confirmed successful + //TODO probably should set caption to QString::null while map is unusable + + #define KMSG( s ) KMessageBox::information( widget(), s ) + + KURL url = u; + url.cleanPath( true ); + const QString path = url.path( 1 ); + const QCString path8bit = QFile::encodeName( path ); + const bool isLocal = url.protocol() == "file"; + + if( url.isEmpty() ) + { + //do nothing, chances are the user accidently pressed ENTER + } + else if( !url.isValid() ) + { + KMSG( i18n( "The entered URL cannot be parsed; it is invalid." ) ); + } + else if( path[0] != '/' ) + { + KMSG( i18n( "Filelight only accepts absolute paths, eg. /%1" ).arg( path ) ); + } + else if( isLocal && access( path8bit, F_OK ) != 0 ) //stat( path, &statbuf ) == 0 + { + KMSG( i18n( "Directory not found: %1" ).arg( path ) ); + } + else if( isLocal && access( path8bit, R_OK | X_OK ) != 0 ) + { + KMSG( i18n( "Unable to enter: %1\nYou do not have access rights to this location." ).arg( path ) ); + } + else + { + if( url == m_url ) + m_manager->emptyCache(); //same as rescan() + + return start( url ); + } + + return false; +} + +bool +Part::closeURL() +{ + if( m_manager->abort() ) + statusBar()->message( i18n( "Aborting Scan..." ) ); + + m_url = KURL(); + + return true; +} + +void +Part::updateURL( const KURL &u ) +{ + //the map has changed internally, update the interface to reflect this + emit m_ext->openURLNotify(); //must be done first + emit m_ext->setLocationBarURL( u.prettyURL() ); + + //do this last, or it breaks Konqi location bar + m_url = u; +} + +void +Part::configFilelight() +{ + QWidget *dialog = new SettingsDialog( widget(), "settings_dialog" ); + + connect( dialog, SIGNAL(canvasIsDirty( int )), m_map, SLOT(refresh( int )) ); + connect( dialog, SIGNAL(mapIsInvalid()), m_manager, SLOT(emptyCache()) ); + + dialog->show(); //deletes itself +} + +KAboutData* +Part::createAboutData() +{ + return new KAboutData( APP_NAME, I18N_NOOP( APP_PRETTYNAME ), APP_VERSION ); +} + +bool +Part::start( const KURL &url ) +{ + if( !m_started ) { + m_statusbar->addStatusBarItem( new ProgressBox( statusBar(), this ), 0, true ); + connect( m_map, SIGNAL(mouseHover( const QString& )), statusBar(), SLOT(message( const QString& )) ); + connect( m_map, SIGNAL(created( const Directory* )), statusBar(), SLOT(clear()) ); + m_started = true; + } + + if( m_manager->start( url ) ) { + m_url = url; + + const QString s = i18n( "Scanning: %1" ).arg( prettyURL() ); + stateChanged( "scan_started" ); + emit started( 0 ); //as a Part, we have to do this + emit setWindowCaption( s ); + statusBar()->message( s ); + m_map->invalidate(); //to maintain ui consistency + + return true; + } + + return false; +} + +void +Part::rescan() +{ + //FIXME we have to empty the cache because otherwise rescan picks up the old tree.. + m_manager->emptyCache(); //causes canvas to invalidate + start( m_url ); +} + +void +Part::scanCompleted( Directory *tree ) +{ + if( tree ) { + statusBar()->message( i18n( "Scan completed, generating map..." ) ); + + m_map->create( tree ); + + //do after creating map + stateChanged( "scan_complete" ); + } + else { + stateChanged( "scan_failed" ); + emit canceled( i18n( "Scan failed: %1" ).arg( prettyURL() ) ); + emit setWindowCaption( QString::null ); + + statusBar()->clear(); +// QTimer::singleShot( 2000, statusBar(), SLOT(clear()) ); + + m_url = KURL(); + } +} + +void +Part::mapChanged( const Directory *tree ) +{ + //IMPORTANT -> m_url has already been set + + emit setWindowCaption( prettyURL() ); + + ProgressBox *progress = static_cast<ProgressBox *>(statusBar()->child( "ProgressBox" )); + + if( progress ) + progress->setText( tree->children() ); +} + +} //namespace Filelight + +#include "part.moc" diff --git a/src/part/part.h b/src/part/part.h new file mode 100644 index 0000000..348b22b --- /dev/null +++ b/src/part/part.h @@ -0,0 +1,71 @@ +// Author: Max Howell <[email protected]>, (C) 2003-4 +// Copyright: See COPYING file that comes with this distribution + +#ifndef FILELIGHTPART_H +#define FILELIGHTPART_H + +#include <kparts/browserextension.h> +#include <kparts/statusbarextension.h> +#include <kparts/part.h> +#include <kurl.h> + +class KAboutData; +using KParts::StatusBarExtension; +namespace RadialMap { class Widget; } +class Directory; + + +namespace Filelight +{ + class Part; + + class BrowserExtension : public KParts::BrowserExtension + { + public: + BrowserExtension( Part*, const char * = 0 ); + }; + + + class Part : public KParts::ReadOnlyPart + { + Q_OBJECT + + public: + Part( QWidget *, const char *, QObject *, const char *, const QStringList& ); + + virtual bool openFile() { return false; } //pure virtual in base class + virtual bool closeURL(); + + QString prettyURL() const { return m_url.protocol() == "file" ? m_url.path() : m_url.prettyURL(); } + + static KAboutData *createAboutData(); + + public slots: + virtual bool openURL( const KURL& ); + void configFilelight(); + void rescan(); + + private slots: + void postInit(); + void scanCompleted( Directory* ); + void mapChanged( const Directory* ); + + private: + KStatusBar *statusBar() { return m_statusbar->statusBar(); } + + BrowserExtension *m_ext; + StatusBarExtension *m_statusbar; + RadialMap::Widget *m_map; + class ScanManager *m_manager; + + bool m_started; + + private: + bool start( const KURL& ); + + private slots: + void updateURL( const KURL & ); + }; +} + +#endif diff --git a/src/part/progressBox.cpp b/src/part/progressBox.cpp new file mode 100644 index 0000000..5bf205a --- /dev/null +++ b/src/part/progressBox.cpp @@ -0,0 +1,65 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include <kglobal.h> +#include <kglobalsettings.h> +#include <kio/job.h> +#include <klocale.h> + +#include "scan.h" +#include "progressBox.h" + + +ProgressBox::ProgressBox( QWidget *parent, QObject *part ) + : QLabel( parent, "ProgressBox" ) +{ + hide(); + + setAlignment( Qt::AlignCenter ); + setFont( KGlobalSettings::fixedFont() ); + setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed ); + + setText( 999999 ); + setMinimumWidth( sizeHint().width() ); + + connect( &m_timer, SIGNAL(timeout()), SLOT(report()) ); + connect( part, SIGNAL(started( KIO::Job* )), SLOT(start()) ); + connect( part, SIGNAL(completed()), SLOT(stop()) ); + connect( part, SIGNAL(canceled( const QString& )), SLOT(halt()) ); +} + +void +ProgressBox::start() //slot +{ + m_timer.start( 50 ); //20 times per second - very smooth + report(); + show(); +} + +void +ProgressBox::report() //slot +{ + setText( Filelight::ScanManager::files() ); +} + +void +ProgressBox::stop() +{ + m_timer.stop(); +} + +void +ProgressBox::halt() +{ + // canceled by stop button + m_timer.stop(); + QTimer::singleShot( 2000, this, SLOT(hide()) ); +} + +void +ProgressBox::setText( int files ) +{ + QLabel::setText( i18n("%n File", "%n Files", files) ); +} + +#include "progressBox.moc" diff --git a/src/part/progressBox.h b/src/part/progressBox.h new file mode 100644 index 0000000..17cc2a6 --- /dev/null +++ b/src/part/progressBox.h @@ -0,0 +1,32 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#ifndef PROGRESSBOX_H +#define PROGRESSBOX_H + +#include <qlabel.h> +#include <qtimer.h> + +namespace KIO { class Job; } + + +class ProgressBox : public QLabel +{ +Q_OBJECT + +public: + ProgressBox( QWidget*, QObject* ); + + void setText( int ); + +public slots: + void start(); + void report(); + void stop(); + void halt(); + +private: + QTimer m_timer; +}; + +#endif diff --git a/src/part/radialMap/Makefile.am b/src/part/radialMap/Makefile.am new file mode 100644 index 0000000..989cbe1 --- /dev/null +++ b/src/part/radialMap/Makefile.am @@ -0,0 +1,4 @@ +INCLUDES = -I$(top_srcdir)/src/part $(all_includes) +METASOURCES = AUTO +noinst_LTLIBRARIES = libradialmap.la +libradialmap_la_SOURCES = widget.cpp builder.cpp map.cpp widgetEvents.cpp labels.cpp segmentTip.cpp diff --git a/src/part/radialMap/builder.cpp b/src/part/radialMap/builder.cpp new file mode 100644 index 0000000..68dc382 --- /dev/null +++ b/src/part/radialMap/builder.cpp @@ -0,0 +1,141 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include "builder.h" +#include "Config.h" +#include "fileTree.h" +#include <kglobal.h> //locale object +#include <klocale.h> +#include "widget.h" + + +//**** REMOVE NEED FOR the +1 with MAX_RING_DEPTH uses +//**** add some angle bounds checking (possibly in Segment ctor? can I delete in a ctor?) +//**** this class is a mess + +RadialMap::Builder::Builder( RadialMap::Map *m, const Directory* const d, bool fast ) + : m_map( m ) + , m_root( d ) + , m_minSize( static_cast<unsigned int>((d->size() * 3) / (PI * m->height() - m->MAP_2MARGIN )) ) + , m_depth( &m->m_visibleDepth ) +{ + m_signature = new Chain<Segment> [*m_depth + 1]; + + if( !fast )//|| *m_depth == 0 ) //depth 0 is special case usability-wise //**** WHY?! + { + //determine depth rather than use old one + findVisibleDepth( d ); //sets m_depth + } + + m_map->setRingBreadth(); + setLimits( m_map->m_ringBreadth ); + build( d ); + + m_map->m_signature = m_signature; + + delete [] m_limits; +} + + +void +RadialMap::Builder::findVisibleDepth( const Directory* const dir, const unsigned int depth ) +{ + //**** because I don't use the same minimumSize criteria as in the visual function + // this can lead to incorrect visual representation + //**** BUT, you can't set those limits until you know m_depth! + + //**** also this function doesn't check to see if anything is actually visible + // it just assumes that when it reaches a new level everything in it is visible + // automatically. This isn't right especially as there might be no files in the + // dir provided to this function! + + static uint stopDepth = 0; + + if( dir == m_root ) + { + stopDepth = *m_depth; + *m_depth = 0; + } + + if( *m_depth < depth ) *m_depth = depth; + if( *m_depth >= stopDepth ) return; + + for( ConstIterator<File> it = dir->constIterator(); it != dir->end(); ++it ) + if( (*it)->isDirectory() && (*it)->size() > m_minSize ) + findVisibleDepth( (Directory *)*it, depth + 1 ); //if no files greater than min size the depth is still recorded +} + +void +RadialMap::Builder::setLimits( const uint &b ) //b = breadth? +{ + double size3 = m_root->size() * 3; + double pi2B = PI * 2 * b; + + m_limits = new uint [*m_depth + 1]; //FIXME delete! + + for( unsigned int d = 0; d <= *m_depth; ++d ) + m_limits[d] = (uint)(size3 / (double)(pi2B * (d + 1))); //min is angle that gives 3px outer diameter for that depth +} + + +//**** segments currently overlap at edges (i.e. end of first is start of next) +bool +RadialMap::Builder::build( const Directory* const dir, const unsigned int depth, unsigned int a_start, const unsigned int a_end ) +{ + //first iteration: dir == m_root + + if( dir->children() == 0 ) //we do fileCount rather than size to avoid chance of divide by zero later + return false; + + uint hiddenSize = 0, hiddenFileCount = 0; + + for( ConstIterator<File> it = dir->constIterator(); it != dir->end(); ++it ) + { + if( (*it)->size() > m_limits[depth] ) + { + unsigned int a_len = (unsigned int)(5760 * ((double)(*it)->size() / (double)m_root->size())); + + Segment *s = new Segment( *it, a_start, a_len ); + + (m_signature + depth)->append( s ); + + if( (*it)->isDirectory() ) + { + if( depth != *m_depth ) + { + //recurse + s->m_hasHiddenChildren = build( (Directory*)*it, depth + 1, a_start, a_start + a_len ); + } + else s->m_hasHiddenChildren = true; + } + + a_start += a_len; //**** should we add 1? + + } else { + + hiddenSize += (*it)->size(); + + if( (*it)->isDirectory() ) //**** considered virtual, but dir wouldn't count itself! + hiddenFileCount += static_cast<const Directory*>(*it)->children(); //need to add one to count the dir as well + + ++hiddenFileCount; + } + } + + if( hiddenFileCount == dir->children() && !Config::showSmallFiles ) + return true; + + else if( (Config::showSmallFiles && hiddenSize > m_limits[depth]) || (depth == 0 && (hiddenSize > dir->size()/8)) /*|| > size() * 0.75*/ ) + { + //append a segment for unrepresented space - a "fake" segment + + // I dunno how to i18n this + const QString s = i18n( "There can't ever be only 1 file", "%1 files, each about %2" ) + .arg( hiddenFileCount ) + .arg( File::humanReadableSize( hiddenSize/hiddenFileCount ) ); + + (m_signature + depth)->append( new Segment( new File( s.local8Bit(), hiddenSize ), a_start, a_end - a_start, true ) ); + } + + return false; +} diff --git a/src/part/radialMap/builder.h b/src/part/radialMap/builder.h new file mode 100644 index 0000000..819813a --- /dev/null +++ b/src/part/radialMap/builder.h @@ -0,0 +1,38 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#ifndef BUILDER_H +#define BUILDER_H + +#include "radialMap.h" //Segment, defines + +template <class T> class Chain; +class Directory; + + +namespace RadialMap +{ + class Map; + + //temporary class that builds the Map signature + + class Builder + { + public: + Builder( Map*, const Directory* const, bool fast=false ); + + private: + void findVisibleDepth( const Directory* const dir, const uint=0 ); + void setLimits( const uint& ); + bool build( const Directory* const, const uint=0, uint=0, const uint=5760 ); + + Map *m_map; + const Directory* const m_root; + const uint m_minSize; + uint *m_depth; + Chain<Segment> *m_signature; + uint *m_limits; + }; +} + +#endif diff --git a/src/part/radialMap/labels.cpp b/src/part/radialMap/labels.cpp new file mode 100644 index 0000000..73a7ba8 --- /dev/null +++ b/src/part/radialMap/labels.cpp @@ -0,0 +1,327 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include <kstringhandler.h> +#include <qfont.h> +#include <qfontmetrics.h> +#include <qpainter.h> +#include <qptrlist.h> + +#include "Config.h" +#include "fileTree.h" +#include "radialMap.h" +#include "sincos.h" +#include "widget.h" + + + +namespace RadialMap +{ + struct Label + { + Label( const RadialMap::Segment *s, int l ) : segment( s ), lvl( l ), a( segment->start() + (segment->length() / 2) ) { } + + bool tooClose( const int &aa ) const { return ( a > aa - LABEL_ANGLE_MARGIN && a < aa + LABEL_ANGLE_MARGIN ); } + + const RadialMap::Segment *segment; + const unsigned int lvl; + const int a; + + int x1, y1, x2, y2, x3; + int tx, ty; + + QString qs; + }; + + class LabelList : public QPtrList<Label> + { + protected: + int compareItems( QPtrCollection::Item item1, QPtrCollection::Item item2 ) + { + //you add 1440 to work round the fact that later you want the circle split vertically + //and as it is you start at 3 o' clock. It's to do with rightPrevY, stops annoying bug + + int a1 = ((Label*)item1)->a + 1440; + int a2 = ((Label*)item2)->a + 1440; + + if( a1 == a2 ) + return 0; + + if( a1 > 5760 ) a1 -= 5760; + if( a2 > 5760 ) a2 -= 5760; + + if( a1 > a2 ) + return 1; + + return -1; + } + }; +} + + +void +RadialMap::Widget::paintExplodedLabels( QPainter &paint ) const +{ + //we are a friend of RadialMap::Map + + LabelList list; list.setAutoDelete( true ); + QPtrListIterator<Label> it( list ); + unsigned int startLevel = 0; + + + //1. Create list of labels sorted in the order they will be rendered + + if( m_focus && m_focus->file() != m_tree ) //separate behavior for selected vs unselected segments + { + //don't bother with files + if( m_focus->file() && !m_focus->file()->isDirectory() ) + return; + + //find the range of levels we will be potentially drawing labels for + //startLevel is the level above whatever m_focus is in + for( const Directory *p = (const Directory*)m_focus->file(); p != m_tree; ++startLevel ) + p = p->parent(); + + //range=2 means 2 levels to draw labels for + + unsigned int a1, a2, minAngle; + + a1 = m_focus->start(); + a2 = m_focus->end(); //boundry angles + minAngle = int(m_focus->length() * LABEL_MIN_ANGLE_FACTOR); + + + #define segment (*it) + #define ring (m_map.m_signature + i) + + //**** Levels should be on a scale starting with 0 + //**** range is a useless parameter + //**** keep a topblock var which is the lowestLevel OR startLevel for identation purposes + for( unsigned int i = startLevel; i <= m_map.m_visibleDepth; ++i ) + for( Iterator<Segment> it = ring->iterator(); it != ring->end(); ++it ) + if( segment->start() >= a1 && segment->end() <= a2 ) + if( segment->length() > minAngle ) + list.inSort( new Label( segment, i ) ); + + #undef ring + #undef segment + + } else { + + #define ring m_map.m_signature + + for( Iterator<Segment> it = ring->iterator(); it != ring->end(); ++it ) + if( (*it)->length() > 288 ) + list.inSort( new Label( (*it), 0 ) ); + + #undef ring + + } + + //2. Check to see if any adjacent labels are too close together + // if so, remove the least significant labels + + it.toFirst(); + QPtrListIterator<Label> jt( it ); + ++jt; + + while( jt ) //**** no need to check _it_ as jt will be NULL if _it_ was too + { + //this method is fairly efficient + + if( (*it)->tooClose( (*jt)->a ) ) { + if( (*it)->lvl > (*jt)->lvl ) { + list.remove( *it ); + it = jt; + } + else + list.remove( *jt ); + } + else + ++it; + + jt = it; + ++jt; + } + + //used in next two steps + bool varySizes; + //**** should perhaps use doubles + int *sizes = new int [ m_map.m_visibleDepth + 1 ]; //**** make sizes an array of floats I think instead (or doubles) + + do + { + //3. Calculate font sizes + + { + //determine current range of levels to draw for + uint range = 0; + + for( it.toFirst(); it != 0; ++it ) + { + uint lvl = (*it)->lvl; + if( lvl > range ) + range = lvl; + + //**** better way would just be to assign if nothing is range + } + + range -= startLevel; //range 0 means 1 level of labels + + varySizes = Config::varyLabelFontSizes && (range != 0); + + if( varySizes ) + { + //create an array of font sizes for various levels + //will exceed normal font pitch automatically if necessary, but not minPitch + //**** this needs to be checked lots + + //**** what if this is negative (min size gtr than default size) + uint step = (paint.font().pointSize() - Config::minFontPitch) / range; + if( step == 0 ) + step = 1; + + for( uint x = range + startLevel, y = Config::minFontPitch; x >= startLevel; y += step, --x ) + sizes[x] = y; + } + } + + //4. determine label co-ordinates + + int x1, y1, x2, y2, x3, tx, ty; //coords + double sinra, cosra, ra; //angles + + int cx = m_map.width() / 2 + m_offset.x(); //centre relative to canvas + int cy = m_map.height() / 2 + m_offset.y(); + + int spacer, preSpacer = int(m_map.m_ringBreadth * 0.5) + m_map.m_innerRadius; + int fullStrutLength = ( m_map.width() - m_map.MAP_2MARGIN ) / 2 + LABEL_MAP_SPACER; //full length of a strut from map center + + int prevLeftY = 0; + int prevRightY = height(); + + bool rightSide; + + QFont font; + + for( it.toFirst(); it != 0; ++it ) + { + //** bear in mind that text is drawn with QPoint param as BOTTOM left corner of text box + QString qs = (*it)->segment->file()->name(); + if( varySizes ) + font.setPointSize( sizes[(*it)->lvl] ); + QFontMetrics fm( font ); + int fmh = fm.height(); //used to ensure label texts don't overlap + int fmhD4 = fmh / 4; + + fmh += LABEL_TEXT_VMARGIN; + + rightSide = ( (*it)->a < 1440 || (*it)->a > 4320 ); + + ra = M_PI/2880 * (*it)->a; //convert to radians + sincos( ra, &sinra, &cosra ); + + + spacer = preSpacer + m_map.m_ringBreadth * (*it)->lvl; + + x1 = cx + (int)(cosra * spacer); + y1 = cy - (int)(sinra * spacer); + y2 = y1 - (int)(sinra * (fullStrutLength - spacer)); + + if( rightSide ) { //righthand side, going upwards + if( y2 > prevRightY /*- fmh*/ ) //then it is too low, needs to be drawn higher + y2 = prevRightY /*- fmh*/; + } + else //lefthand side, going downwards + if( y2 < prevLeftY/* + fmh*/ ) //then we're too high, need to be drawn lower + y2 = prevLeftY /*+ fmh*/; + + x2 = x1 - int(double(y2 - y1) / tan( ra )); + ty = y2 + fmhD4; + + + if( rightSide ) { + if( x2 > width() || ty < fmh || x2 < x1 ) { + //skip this strut + //**** don't duplicate this code + list.remove( *it ); //will delete the label and set it to list.current() which _should_ be the next ptr + break; + } + + prevRightY = ty - fmh - fmhD4; //must be after above's "continue" + + qs = KStringHandler::cPixelSqueeze( qs, fm, width() - x2 ); + + x3 = width() - fm.width( qs ) + - LABEL_HMARGIN //outer margin + - LABEL_TEXT_HMARGIN //margin between strut and text + //- ((*it)->lvl - startLevel) * LABEL_HMARGIN; //indentation + ; + if( x3 < x2 ) x3 = x2; + tx = x3 + LABEL_TEXT_HMARGIN; + + } else { + + if( x2 < 0 || ty > height() || x2 > x1 ) + { + //skip this strut + list.remove( *it ); //will delete the label and set it to list.current() which _should_ be the next ptr + break; + } + + prevLeftY = ty + fmh - fmhD4; + + qs = KStringHandler::cPixelSqueeze( qs, fm, x2 ); + + //**** needs a little tweaking: + + tx = fm.width( qs ) + LABEL_HMARGIN/* + ((*it)->lvl - startLevel) * LABEL_HMARGIN*/; + if( tx > x2 ) { //text is too long + tx = LABEL_HMARGIN + x2 - tx; //some text will be lost from sight + x3 = x2; //no text margin (right side of text here) + } else { + x3 = tx + LABEL_TEXT_HMARGIN; + tx = LABEL_HMARGIN /*+ ((*it)->lvl - startLevel) * LABEL_HMARGIN*/; + } + } + + (*it)->x1 = x1; + (*it)->y1 = y1; + (*it)->x2 = x2; + (*it)->y2 = y2; + (*it)->x3 = x3; + (*it)->tx = tx; + (*it)->ty = ty; + (*it)->qs = qs; + } + + //if an element is deleted at this stage, we need to do this whole + //iteration again, thus the following loop + //**** in rare case that deleted label was last label in top level + // and last in labelList too, this will not work as expected (not critical) + + } while( it != 0 ); + + + //5. Render labels + + paint.setPen( QPen( Qt::black, 1 ) ); + + for( it.toFirst(); it != 0; ++it ) + { + if( varySizes ) { + //**** how much overhead in making new QFont each time? + // (implicate sharing remember) + QFont font = paint.font(); + font.setPointSize( sizes[(*it)->lvl] ); + paint.setFont( font ); + } + + paint.drawEllipse( (*it)->x1 - 3, (*it)->y1 - 3, 7, 7 ); //**** CPU intensive! better to use a pixmap + paint.drawLine( (*it)->x1, (*it)->y1, (*it)->x2, (*it)->y2 ); + paint.drawLine( (*it)->x2, (*it)->y2, (*it)->x3, (*it)->y2); + paint.drawText( (*it)->tx, (*it)->ty, (*it)->qs ); + } + + delete [] sizes; +} diff --git a/src/part/radialMap/map.cpp b/src/part/radialMap/map.cpp new file mode 100644 index 0000000..8eb8f02 --- /dev/null +++ b/src/part/radialMap/map.cpp @@ -0,0 +1,442 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include <kcursor.h> //make() +#include <kglobalsettings.h> //kdeColours +#include <kimageeffect.h> //desaturate() +#include <qapplication.h> //make() +#include <qimage.h> //make() & paint() +#include <qfont.h> //ctor +#include <qfontmetrics.h> //ctor +#include <qpainter.h> + +#include "builder.h" +#include "Config.h" +#include "debug.h" +#include "fileTree.h" +#define SINCOS_H_IMPLEMENTATION (1) +#include "sincos.h" +#include "widget.h" + +#define COLOR_GREY QColor( 0, 0, 140, QColor::Hsv ) + + +RadialMap::Map::Map() + : m_signature( 0 ) + , m_ringBreadth( MIN_RING_BREADTH ) + , m_innerRadius( 0 ) + , m_visibleDepth( DEFAULT_RING_DEPTH ) +{ + //FIXME this is all broken. No longer is a maximum depth! + const int fmh = QFontMetrics( QFont() ).height(); + const int fmhD4 = fmh / 4; + MAP_2MARGIN = 2 * ( fmh - (fmhD4 - LABEL_MAP_SPACER) ); //margin is dependent on fitting in labels at top and bottom +} + +RadialMap::Map::~Map() +{ + delete [] m_signature; +} + +void +RadialMap::Map::invalidate( const bool desaturateTheImage ) +{ + DEBUG_ANNOUNCE + + delete [] m_signature; + m_signature = 0; + + if( desaturateTheImage ) + { + QImage img = this->convertToImage(); + + KImageEffect::desaturate( img, 0.7 ); + KImageEffect::toGray( img, true ); + + this->convertFromImage( img ); + } + + m_visibleDepth = Config::defaultRingDepth; +} + +void +RadialMap::Map::make( const Directory *tree, bool refresh ) +{ + DEBUG_ANNOUNCE + + //**** determineText seems pointless optimisation + // but is it good to keep the text consistent? + // even if it makes it a lie? + + //slow operation so set the wait cursor + QApplication::setOverrideCursor( KCursor::waitCursor() ); + + { + //build a signature of visible components + delete [] m_signature; + Builder builder( this, tree, refresh ); + } + + //colour the segments + colorise(); + + //determine centerText + if( !refresh ) + { + int i; + for( i = 2; i > 0; --i ) + if( tree->size() > File::DENOMINATOR[i] ) + break; + + m_centerText = tree->humanReadableSize( (File::UnitPrefix)i ); + } + + //paint the pixmap + aaPaint(); + + QApplication::restoreOverrideCursor(); +} + +void +RadialMap::Map::setRingBreadth() +{ + DEBUG_ANNOUNCE + + //FIXME called too many times on creation + + m_ringBreadth = (height() - MAP_2MARGIN) / (2 * m_visibleDepth + 4); + + if( m_ringBreadth < MIN_RING_BREADTH ) + m_ringBreadth = MIN_RING_BREADTH; + + else if( m_ringBreadth > MAX_RING_BREADTH ) + m_ringBreadth = MAX_RING_BREADTH; +} + +bool +RadialMap::Map::resize( const QRect &rect ) +{ + DEBUG_ANNOUNCE + + //there's a MAP_2MARGIN border + + #define mw width() + #define mh height() + #define cw rect.width() + #define ch rect.height() + + if( cw < mw || ch < mh || (cw > mw && ch > mh) ) + { + uint size = (( cw < ch ) ? cw : ch) - MAP_2MARGIN; + + //this also causes uneven sizes to always resize when resizing but map is small in that dimension + //size -= size % 2; //even sizes mean less staggered non-antialiased resizing + + { + const uint minSize = MIN_RING_BREADTH * 2 * (m_visibleDepth + 2); + const uint mD2 = MAP_2MARGIN / 2; + + if( size < minSize ) size = minSize; + + //this QRect is used by paint() + m_rect.setRect( mD2, mD2, size, size ); + } + + //resize the pixmap + size += MAP_2MARGIN; + KPixmap::resize( size, size ); + + // for summary widget this is a good optimisation as it happens + if (KPixmap::isNull()) + return false; + + if( m_signature != 0 ) + { + setRingBreadth(); + paint(); + } + else fill(); //FIXME I don't like having to do this.. + + return true; + } + + #undef mw + #undef mh + #undef cw + #undef ch + + return false; +} + +void +RadialMap::Map::colorise() +{ + DEBUG_ANNOUNCE + + QColor cp, cb; + double darkness = 1; + double contrast = (double)Config::contrast / (double)100; + int h, s1, s2, v1, v2; + + QColor kdeColour[2] = { KGlobalSettings::inactiveTitleColor(), KGlobalSettings::activeTitleColor() }; + + double deltaRed = (double)(kdeColour[0].red() - kdeColour[1].red()) / 2880; //2880 for semicircle + double deltaGreen = (double)(kdeColour[0].green() - kdeColour[1].green()) / 2880; + double deltaBlue = (double)(kdeColour[0].blue() - kdeColour[1].blue()) / 2880; + + for( uint i = 0; i <= m_visibleDepth; ++i, darkness += 0.04 ) + { + for( Iterator<Segment> it = m_signature[i].iterator(); it != m_signature[i].end(); ++it ) + { + switch( Config::scheme ) + { + case 2000: //HACK for summary view + + if( (*it)->file()->name() == "Used" ) { + cb = QApplication::palette().active().color( QColorGroup::Highlight ); + cb.getHsv( &h, &s1, &v1 ); + + if( s1 > 80 ) + s1 = 80; + + v2 = v1 - int(contrast * v1); + s2 = s1 + int(contrast * (255 - s1)); + + cb.setHsv( h, s1, v1 ); + cp.setHsv( h, s2, v2 ); + } + else { + cp = Qt::gray; + cb = Qt::white; + } + + (*it)->setPalette( cp, cb ); + + continue; + + case Filelight::KDE: + { + //gradient will work by figuring out rgb delta values for 360 degrees + //then each component is angle*delta + + int a = (*it)->start(); + + if( a > 2880 ) a = 2880 - (a - 2880); + + h = (int)(deltaRed * a) + kdeColour[1].red(); + s1 = (int)(deltaGreen * a) + kdeColour[1].green(); + v1 = (int)(deltaBlue * a) + kdeColour[1].blue(); + + cb.setRgb( h, s1, v1 ); + cb.getHsv( &h, &s1, &v1 ); + + break; + } + + case Filelight::HighContrast: + + cp.setHsv( 0, 0, 0 ); //values of h, s and v are irrelevant + cb.setHsv( 180, 0, int(255.0 * contrast) ); + (*it)->setPalette( cp, cb ); + continue; + + default: + h = int((*it)->start() / 16); + s1 = 160; + v1 = (int)(255.0 / darkness); //****doing this more often than once seems daft! + } + + v2 = v1 - int(contrast * v1); + s2 = s1 + int(contrast * (255 - s1)); + + if( s1 < 80 ) s1 = 80; //can fall too low and makes contrast between the files hard to discern + + if( (*it)->isFake() ) //multi-file + { + cb.setHsv( h, s2, (v2 < 90) ? 90 : v2 ); //too dark if < 100 + cp.setHsv( h, 17, v1 ); + } + else if( !(*it)->file()->isDirectory() ) //file + { + cb.setHsv( h, 17, v1 ); + cp.setHsv( h, 17, v2 ); + } + else //directory + { + cb.setHsv( h, s1, v1 ); //v was 225 + cp.setHsv( h, s2, v2 ); //v was 225 - delta + } + + (*it)->setPalette( cp, cb ); + + //**** may be better to store KDE colours as H and S and vary V as others + //**** perhaps make saturation difference for s2 dependent on contrast too + //**** fake segments don't work with highContrast + //**** may work better with cp = cb rather than Qt::white + //**** you have to ensure the grey of files is sufficient, currently it works only with rainbow (perhaps use contrast there too) + //**** change v1,v2 to vp, vb etc. + //**** using percentages is not strictly correct as the eye doesn't work like that + //**** darkness factor is not done for kde_colour scheme, and also value for files is incorrect really for files in this scheme as it is not set like rainbow one is + } + } +} + +void +RadialMap::Map::aaPaint() +{ + //paint() is called during continuous processes + //aaPaint() is not and is slower so set overidecursor (make sets it too) + QApplication::setOverrideCursor( KCursor::waitCursor() ); + paint( Config::antiAliasFactor ); + QApplication::restoreOverrideCursor(); +} + +void +RadialMap::Map::paint( unsigned int scaleFactor ) +{ + DEBUG_ANNOUNCE + + if (scaleFactor == 0) //just in case + scaleFactor = 1; + + QPainter paint; + QRect rect = m_rect; + int step = m_ringBreadth; + int excess = -1; + + //scale the pixmap, or do intelligent distribution of excess to prevent nasty resizing + if( scaleFactor > 1 ) + { + int x1, y1, x2, y2; + rect.coords( &x1, &y1, &x2, &y2 ); + x1 *= scaleFactor; + y1 *= scaleFactor; + x2 *= scaleFactor; + y2 *= scaleFactor; + rect.setCoords( x1, y1, x2, y2 ); + + step *= scaleFactor; + KPixmap::resize( this->size() * (int)scaleFactor ); + } + else if( m_ringBreadth != MAX_RING_BREADTH && m_ringBreadth != MIN_RING_BREADTH ) { + excess = rect.width() % m_ringBreadth; + ++step; + } + + //**** best option you can think of is to make the circles slightly less perfect, + // ** i.e. slightly eliptic when resizing inbetween + + if (KPixmap::isNull()) + return; + + paint.begin( this ); + + fill(); //erase background + + for( int x = m_visibleDepth; x >= 0; --x ) + { + int width = rect.width() / 2; + //clever geometric trick to find largest angle that will give biggest arrow head + int a_max = int(acos( (double)width / double((width + 5) * scaleFactor) ) * (180*16 / M_PI)); + + for( ConstIterator<Segment> it = m_signature[x].constIterator(); it != m_signature[x].end(); ++it ) + { + //draw the pie segments, most of this code is concerned with drawing the little + //arrows on the ends of segments when they have hidden files + + paint.setPen( (*it)->pen() ); + + if( (*it)->hasHiddenChildren() ) + { + //draw arrow head to indicate undisplayed files/directories + QPointArray pts( 3 ); + QPoint pos, cpos = rect.center(); + int a[3] = { (*it)->start(), (*it)->length(), 0 }; + + a[2] = a[0] + (a[1] / 2); //assign to halfway between + if( a[1] > a_max ) + { + a[1] = a_max; + a[0] = a[2] - a_max / 2; + } + + a[1] += a[0]; + + for( int i = 0, radius = width; i < 3; ++i ) + { + double ra = M_PI/(180*16) * a[i], sinra, cosra; + + if( i == 2 ) + radius += 5 * scaleFactor; + sincos( ra, &sinra, &cosra ); + pos.rx() = cpos.x() + static_cast<int>(cosra * radius); + pos.ry() = cpos.y() - static_cast<int>(sinra * radius); + pts.setPoint( i, pos ); + } + + paint.setBrush( (*it)->pen() ); + paint.drawPolygon( pts ); + } + + paint.setBrush( (*it)->brush() ); + paint.drawPie( rect, (*it)->start(), (*it)->length() ); + + if( (*it)->hasHiddenChildren() ) + { + //**** code is bloated! + paint.save(); + QPen pen = paint.pen(); + int width = 2 * scaleFactor; + pen.setWidth( width ); + paint.setPen( pen ); + QRect rect2 = rect; + width /= 2; + rect2.addCoords( width, width, -width, -width ); + paint.drawArc( rect2, (*it)->start(), (*it)->length() ); + paint.restore(); + } + } + + if( excess >= 0 ) { //excess allows us to resize more smoothly (still crud tho) + if( excess < 2 ) //only decrease rect by more if even number of excesses left + --step; + excess -= 2; + } + + rect.addCoords( step, step, -step, -step ); + } + + // if( excess > 0 ) rect.addCoords( excess, excess, 0, 0 ); //ugly + + paint.setPen( COLOR_GREY ); + paint.setBrush( Qt::white ); + paint.drawEllipse( rect ); + + if( scaleFactor > 1 ) + { + //have to end in order to smoothscale() + paint.end(); + + int x1, y1, x2, y2; + rect.coords( &x1, &y1, &x2, &y2 ); + x1 /= scaleFactor; + y1 /= scaleFactor; + x2 /= scaleFactor; + y2 /= scaleFactor; + rect.setCoords( x1, y1, x2, y2 ); + + QImage img = this->convertToImage(); + img = img.smoothScale( this->size() / (int)scaleFactor ); + this->convertFromImage( img ); + + paint.begin( this ); + paint.setPen( COLOR_GREY ); + paint.setBrush( Qt::white ); + } + + paint.drawText( rect, Qt::AlignCenter, m_centerText ); + + m_innerRadius = rect.width() / 2; //rect.width should be multiple of 2 + + paint.end(); +} diff --git a/src/part/radialMap/radialMap.h b/src/part/radialMap/radialMap.h new file mode 100644 index 0000000..5023b89 --- /dev/null +++ b/src/part/radialMap/radialMap.h @@ -0,0 +1,72 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#ifndef RADIALMAP_H +#define RADIALMAP_H + +#include <qcolor.h> + +class File; + + +namespace RadialMap +{ + class Segment //all angles are in 16ths of degrees + { + public: + Segment( const File *f, uint s, uint l, bool isFake = false ) + : m_angleStart( s ) + , m_angleSegment( l ) + , m_file( f ) + , m_hasHiddenChildren( false ) + , m_fake( isFake ) {} + ~Segment(); + + uint start() const { return m_angleStart; } + uint length() const { return m_angleSegment; } + uint end() const { return m_angleStart + m_angleSegment; } + const File *file() const { return m_file; } + const QColor& pen() const { return m_pen; } + const QColor& brush() const { return m_brush; } + + bool isFake() const { return m_fake; } + bool hasHiddenChildren() const { return m_hasHiddenChildren; } + + bool intersects( uint a ) const { return ( ( a >= start() ) && ( a < end() ) ); } + + friend class Map; + friend class Builder; + + private: + void setPalette( const QColor &p, const QColor &b ) { m_pen = p; m_brush = b; } + + const uint m_angleStart, m_angleSegment; + const File* const m_file; + QColor m_pen, m_brush; + bool m_hasHiddenChildren; + const bool m_fake; + }; +} + + +#ifndef PI +#define PI 3.141592653589793 +#endif +#ifndef M_PI +#define M_PI 3.14159265358979323846264338327 +#endif + +#define MIN_RING_BREADTH 20 +#define MAX_RING_BREADTH 60 +#define DEFAULT_RING_DEPTH 4 //first level = 0 +#define MIN_RING_DEPTH 0 + +#define LABEL_MAP_SPACER 7 +#define LABEL_HMARGIN 10 +#define LABEL_TEXT_HMARGIN 5 +#define LABEL_TEXT_VMARGIN 0 +#define LABEL_ANGLE_MARGIN 32 +#define LABEL_MIN_ANGLE_FACTOR 0.05 +#define LABEL_MAX_CHARS 30 + +#endif diff --git a/src/part/radialMap/segmentTip.cpp b/src/part/radialMap/segmentTip.cpp new file mode 100644 index 0000000..f73e845 --- /dev/null +++ b/src/part/radialMap/segmentTip.cpp @@ -0,0 +1,186 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include "fileTree.h" +#include "segmentTip.h" + +#include <cstdlib> +#include <kapplication.h> //installing eventFilters +#include <kglobal.h> +#include <kglobalsettings.h> +#include <klocale.h> +#include <kpixmapeffect.h> +#include <qpainter.h> +#include <qtooltip.h> //for its palette + + + +namespace RadialMap { + + +bool isBackingStoreActive() +{ + // # xdpyinfo | grep backing + // options: backing-store YES, save-unders YES + + char buffer[4096]; + FILE *xdpyinfo = popen( "xdpyinfo", "r" ); + int const N = fread( (void*)buffer, sizeof(char), 4096, xdpyinfo ); + buffer[ N ] = '\0'; + pclose( xdpyinfo ); + + return QString::fromLocal8Bit( buffer ).contains( "backing-store YES" ); +} + + +SegmentTip::SegmentTip( uint h ) + : QWidget( 0, 0, WNoAutoErase | WStyle_Customize | WStyle_NoBorder | WStyle_Tool | WStyle_StaysOnTop | WX11BypassWM ) + , m_cursorHeight( -h ) + , m_backing_store( isBackingStoreActive() ) +{ + setBackgroundMode( Qt::NoBackground ); +} + +void +SegmentTip::moveTo( QPoint p, const QWidget &canvas, bool placeAbove ) +{ + //**** this function is very slow and seems to be visibly influenced by operations like mapFromGlobal() (who knows why!) + // ** so any improvements are much desired + + //TODO uints could improve the class + p.rx() -= rect().center().x(); + p.ry() -= (placeAbove ? 8 + height() : m_cursorHeight - 8); + + const QRect screen = KGlobalSettings::desktopGeometry( parentWidget() ); + + const int x = p.x(); + const int y = p.y(); + const int x2 = x + width(); + const int y2 = y + height(); //how's it ever gunna get below screen height?! (well you never know I spose) + const int sw = screen.width(); + const int sh = screen.height(); + + if( x < 0 ) p.setX( 0 ); + if( y < 0 ) p.setY( 0 ); + if( x2 > sw ) p.rx() -= x2 - sw; + if( y2 > sh ) p.ry() -= y2 - sh; + + + //I'm using this QPoint to determine where to offset the bitBlt in m_pixmap + QPoint offset = canvas.mapToGlobal( QPoint() ) - p; + if( offset.x() < 0 ) offset.setX( 0 ); + if( offset.y() < 0 ) offset.setY( 0 ); + + + const QRect alphaMaskRect( canvas.mapFromGlobal( p ), size() ); + const QRect intersection( alphaMaskRect.intersect( canvas.rect() ) ); + + m_pixmap.resize( size() ); //move to updateTip once you are sure it can never be null + bitBlt( &m_pixmap, offset, &canvas, intersection, Qt::CopyROP ); + + QColor const c = QToolTip::palette().color( QPalette::Active, QColorGroup::Background ); + if (!m_backing_store) + m_pixmap.fill( c ); + + QPainter paint( &m_pixmap ); + paint.setPen( Qt::black ); + paint.setBrush( Qt::NoBrush ); + paint.drawRect( rect() ); + paint.end(); + + if (m_backing_store) + m_pixmap = KPixmapEffect::fade( m_pixmap, 0.6, c ); + + paint.begin( &m_pixmap ); + paint.drawText( rect(), AlignCenter, m_text ); + paint.end(); + + p += screen.topLeft(); //for Xinerama users + + move( x, y ); + show(); + update(); +} + +void +SegmentTip::updateTip( const File* const file, const Directory* const root ) +{ + const QString s1 = file->fullPath( root ); + QString s2 = file->humanReadableSize(); + KLocale *loc = KGlobal::locale(); + const uint MARGIN = 3; + const uint pc = 100 * file->size() / root->size(); + uint maxw = 0; + uint h = fontMetrics().height()*2 + 2*MARGIN; + + if( pc > 0 ) s2 += QString( " (%1%)" ).arg( loc->formatNumber( pc, 0 ) ); + + m_text = s1; + m_text += '\n'; + m_text += s2; + + if( file->isDirectory() ) + { + double files = static_cast<const Directory*>(file)->children(); + const uint pc = uint((100 * files) / (double)root->children()); + QString s3 = i18n( "Files: %1" ).arg( loc->formatNumber( files, 0 ) ); + + if( pc > 0 ) s3 += QString( " (%1%)" ).arg( loc->formatNumber( pc, 0 ) ); + + maxw = fontMetrics().width( s3 ); + h += fontMetrics().height(); + m_text += '\n'; + m_text += s3; + } + + uint + w = fontMetrics().width( s1 ); if( w > maxw ) maxw = w; + w = fontMetrics().width( s2 ); if( w > maxw ) maxw = w; + + resize( maxw + 2 * MARGIN, h ); +} + +bool +SegmentTip::event( QEvent *e ) +{ + switch( e->type() ) + { + case QEvent::Show: + kapp->installEventFilter( this ); + break; + case QEvent::Hide: + kapp->removeEventFilter( this ); + break; + case QEvent::Paint: + { + //QPainter( this ).drawPixmap( 0, 0, m_pixmap ); + bitBlt( this, 0, 0, &m_pixmap ); + return true; + } + default: + ; + } + + return false/*QWidget::event( e )*/; +} + +bool +SegmentTip::eventFilter( QObject*, QEvent *e ) +{ + switch ( e->type() ) + { + case QEvent::Leave: +// case QEvent::MouseButtonPress: +// case QEvent::MouseButtonRelease: + case QEvent::KeyPress: + case QEvent::KeyRelease: + case QEvent::FocusIn: + case QEvent::FocusOut: + case QEvent::Wheel: + hide(); //FALL THROUGH + default: + return false; //allow this event to passed to target + } +} + +} //namespace RadialMap diff --git a/src/part/radialMap/segmentTip.h b/src/part/radialMap/segmentTip.h new file mode 100644 index 0000000..8bc479e --- /dev/null +++ b/src/part/radialMap/segmentTip.h @@ -0,0 +1,34 @@ +// Author: Max Howell <[email protected]>, (C) 2004 +// Copyright: See COPYING file that comes with this distribution + +#ifndef SEGMENTTIP_H +#define SEGMENTTIP_H + +#include <kpixmap.h> +#include <qwidget.h> + +class File; +class Directory; + +namespace RadialMap +{ + class SegmentTip : public QWidget + { + public: + SegmentTip( uint ); + + void updateTip( const File*, const Directory* ); + void moveTo( QPoint, const QWidget&, bool ); + + private: + virtual bool eventFilter( QObject*, QEvent* ); + virtual bool event( QEvent* ); + + uint m_cursorHeight; + KPixmap m_pixmap; + QString m_text; + bool m_backing_store; + }; +} + +#endif diff --git a/src/part/radialMap/sincos.h b/src/part/radialMap/sincos.h new file mode 100644 index 0000000..b3d8c9f --- /dev/null +++ b/src/part/radialMap/sincos.h @@ -0,0 +1,25 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#ifndef SINCOS_H +#define SINCOS_H + +#include <math.h> + +#if !defined(__GLIBC__) || (__GLIBC__ < 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ < 1) + + void + sincos( int angleRadians, double *Sin, double *Cos ); + +#ifdef SINCOS_H_IMPLEMENTATION + void + sincos( int angleRadians, double *Sin, double *Cos ) + { + *Sin = sin( angleRadians ); + *Cos = cos( angleRadians ); + } +#endif + +#endif + +#endif diff --git a/src/part/radialMap/widget.cpp b/src/part/radialMap/widget.cpp new file mode 100644 index 0000000..9c82c53 --- /dev/null +++ b/src/part/radialMap/widget.cpp @@ -0,0 +1,187 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include <kcursor.h> //ctor +#include <klocale.h> +#include <kurl.h> +#include <qapplication.h> //sendEvent +#include <qbitmap.h> //ctor - finding cursor size +#include <qcursor.h> //slotPostMouseEvent() +#include <qtimer.h> //member + +#include "Config.h" +#include "debug.h" +#include "fileTree.h" +#include "radialMap.h" //constants +#include "widget.h" + + + +RadialMap::Widget::Widget( QWidget *parent, const char *name ) + : QWidget( parent, name, Qt::WNoAutoErase ) + , m_tree( 0 ) + , m_focus( 0 ) + , m_rootSegment( 0 ) //TODO we don't delete it, *shrug* +{ + setAcceptDrops( true ); + setBackgroundColor( Qt::white ); + const QBitmap *cursor = KCursor::handCursor().bitmap(); + m_tip = new SegmentTip(cursor ? cursor->height() : 16); + + connect( this, SIGNAL(created( const Directory* )), SLOT(sendFakeMouseEvent()) ); + connect( this, SIGNAL(created( const Directory* )), SLOT(update()) ); + connect( &m_timer, SIGNAL(timeout()), SLOT(resizeTimeout()) ); +} + +QString +RadialMap::Widget::path() const +{ + return m_tree->fullPath(); +} + +KURL +RadialMap::Widget::url( File const * const file ) const +{ + return KURL::fromPathOrURL( file ? file->fullPath() : m_tree->fullPath() ); +} + +void +RadialMap::Widget::invalidate( const bool b ) +{ + if( isValid() ) + { + //**** have to check that only way to invalidate is this function frankly + //**** otherwise you may get bugs.. + + //disable mouse tracking + setMouseTracking( false ); + + //ensure this class won't think we have a map still + m_tree = 0; + m_focus = 0; + + delete m_rootSegment; + m_rootSegment = 0; + + //FIXME move this disablement thing no? + // it is confusing in other areas, like the whole createFromCache() thing + m_map.invalidate( b ); //b signifies whether the pixmap is made to look disabled or not + if( b ) + update(); + + //tell rest of Filelight + emit invalidated( url() ); + } +} + +void +RadialMap::Widget::create( const Directory *tree ) +{ + //it is not the responsibility of create() to invalidate first + //skip invalidation at your own risk + + //FIXME make it the responsibility of create to invalidate first + + if( tree ) + { + //generate the filemap image + m_map.make( tree ); + + //this is the inner circle in the center + m_rootSegment = new Segment( tree, 0, 16*360 ); + + setMouseTracking( true ); + } + + m_tree = tree; + + //tell rest of Filelight + emit created( tree ); +} + +void +RadialMap::Widget::createFromCache( const Directory *tree ) +{ + //no scan was necessary, use cached tree, however we MUST still emit invalidate + invalidate( false ); + create( tree ); +} + +void +RadialMap::Widget::sendFakeMouseEvent() //slot +{ + QMouseEvent me( QEvent::MouseMove, mapFromGlobal( QCursor::pos() ), Qt::NoButton, Qt::NoButton ); + QApplication::sendEvent( this, &me ); +} + +void +RadialMap::Widget::resizeTimeout() //slot +{ + // the segments are about to erased! + // this was a horrid bug, and proves the OO programming should be obeyed always! + m_focus = 0; + if( m_tree ) + m_map.make( m_tree, true ); + update(); +} + +void +RadialMap::Widget::refresh( int filth ) +{ + //TODO consider a more direct connection + + if( !m_map.isNull() ) + { + switch( filth ) + { + case 1: + m_map.make( m_tree, true ); //true means refresh only + break; + + case 2: + m_map.aaPaint(); + break; + + case 3: + m_map.colorise(); //FALL THROUGH! + case 4: + m_map.paint(); + + default: + break; + } + + update(); + } +} + +void +RadialMap::Widget::zoomIn() //slot +{ + if( m_map.m_visibleDepth > MIN_RING_DEPTH ) + { + --m_map.m_visibleDepth; + m_map.make( m_tree ); + Config::defaultRingDepth = m_map.m_visibleDepth; + update(); + } +} + +void +RadialMap::Widget::zoomOut() //slot +{ + ++m_map.m_visibleDepth; + m_map.make( m_tree ); + if( m_map.m_visibleDepth > Config::defaultRingDepth ) + Config::defaultRingDepth = m_map.m_visibleDepth; + update(); +} + + +RadialMap::Segment::~Segment() +{ + if( isFake() ) + delete m_file; //created by us in Builder::build() +} + +#include "widget.moc" diff --git a/src/part/radialMap/widget.h b/src/part/radialMap/widget.h new file mode 100644 index 0000000..6fdf0e2 --- /dev/null +++ b/src/part/radialMap/widget.h @@ -0,0 +1,114 @@ +//Author: Max Howell <[email protected]>, (C) 2004 +//Copyright: See COPYING file that comes with this distribution + +#ifndef WIDGET_H +#define WIDGET_H + +#include <kurl.h> +#include <qtimer.h> +#include "segmentTip.h" + +template <class T> class Chain; +class Directory; +class File; +namespace KIO { class Job; } +class KURL; + +namespace RadialMap +{ + class Segment; + + class Map : public KPixmap + { + public: + Map(); + ~Map(); + + void make( const Directory *, bool = false ); + bool resize( const QRect& ); + + bool isNull() const { return ( m_signature == 0 ); } + void invalidate( const bool ); + + friend class Builder; + friend class Widget; + + private: + void paint( uint = 1 ); + void aaPaint(); + void colorise(); + void setRingBreadth(); + + Chain<Segment> *m_signature; + + QRect m_rect; + uint m_ringBreadth; ///ring breadth + uint m_innerRadius; ///radius of inner circle + uint m_visibleDepth; ///visible level depth of system + QString m_centerText; + + uint MAP_2MARGIN; + }; + + class Widget : public QWidget + { + Q_OBJECT + + public: + Widget( QWidget* = 0, const char* = 0 ); + ~Widget() { delete m_tip; } + + QString path() const; + KURL url( File const * const = 0 ) const; + + bool isValid() const { return m_tree != 0; } + + friend class Label; //FIXME badness + + public slots: + void zoomIn(); + void zoomOut(); + void create( const Directory* ); + void invalidate( const bool = true ); + void refresh( int ); + + private slots: + void resizeTimeout(); + void sendFakeMouseEvent(); + void deleteJobFinished( KIO::Job* ); + void createFromCache( const Directory* ); + + signals: + void activated( const KURL& ); + void invalidated( const KURL& ); + void created( const Directory* ); + void mouseHover( const QString& ); + void giveMeTreeFor( const KURL& ); + + protected: + virtual void paintEvent( QPaintEvent* ); + virtual void resizeEvent( QResizeEvent* ); + virtual void mouseMoveEvent( QMouseEvent* ); + virtual void mousePressEvent( QMouseEvent* ); + virtual void dragEnterEvent( QDragEnterEvent* ); + virtual void dropEvent( QDropEvent* ); + + protected: + const Segment *segmentAt( QPoint& ) const; //FIXME const reference for a library others can use + const Segment *rootSegment() const { return m_rootSegment; } ///never == 0 + const Segment *focusSegment() const { return m_focus; } ///0 == nothing in focus + + private: + void paintExplodedLabels( QPainter& ) const; + + const Directory *m_tree; + const Segment *m_focus; + QPoint m_offset; + QTimer m_timer; + Map m_map; + SegmentTip *m_tip; + Segment *m_rootSegment; + }; +} + +#endif diff --git a/src/part/radialMap/widgetEvents.cpp b/src/part/radialMap/widgetEvents.cpp new file mode 100644 index 0000000..71db1c0 --- /dev/null +++ b/src/part/radialMap/widgetEvents.cpp @@ -0,0 +1,275 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include "fileTree.h" +#include "radialMap.h" //class Segment +#include "widget.h" + +#include <cmath> //::segmentAt() +#include <kcursor.h> //::mouseMoveEvent() +#include <kiconeffect.h> //::mousePressEvent() +#include <kiconloader.h> //::mousePressEvent() +#include <kio/job.h> //::mousePressEvent() +#include <klocale.h> +#include <kmessagebox.h> //::mousePressEvent() +#include <kpopupmenu.h> //::mousePressEvent() +#include <krun.h> //::mousePressEvent() +#include <kurldrag.h> +#include <qapplication.h>//QApplication::setOverrideCursor() +#include <qclipboard.h> +#include <qpainter.h> +#include <qtimer.h> //::resizeEvent() + + + +void +RadialMap::Widget::resizeEvent( QResizeEvent* ) +{ + if( m_map.resize( rect() ) ) + m_timer.start( 500, true ); //will cause signature to rebuild for new size + + //always do these as they need to be initialised on creation + m_offset.rx() = (width() - m_map.width()) / 2; + m_offset.ry() = (height() - m_map.height()) / 2; +} + +void +RadialMap::Widget::paintEvent( QPaintEvent* ) +{ + //bltBit for some Qt setups will bitBlt _after_ the labels are painted. Which buggers things up! + //shame as bitBlt is faster, possibly Qt bug? Should report the bug? - seems to be race condition + //bitBlt( this, m_offset, &m_map ); + + QPainter paint( this ); + + paint.drawPixmap( m_offset, m_map ); + + //vertical strips + if( m_map.width() < width() ) + { + paint.eraseRect( 0, 0, m_offset.x(), height() ); + paint.eraseRect( m_map.width() + m_offset.x(), 0, m_offset.x() + 1, height() ); + } + //horizontal strips + if( m_map.height() < height() ) + { + paint.eraseRect( 0, 0, width(), m_offset.y() ); + paint.eraseRect( 0, m_map.height() + m_offset.y(), width(), m_offset.y() + 1 ); + } + + //exploded labels + if( !m_map.isNull() && !m_timer.isActive() ) + paintExplodedLabels( paint ); +} + +const RadialMap::Segment* +RadialMap::Widget::segmentAt( QPoint &e ) const +{ + //determine which segment QPoint e is above + + e -= m_offset; + + if( !m_map.m_signature ) + return 0; + + if( e.x() <= m_map.width() && e.y() <= m_map.height() ) + { + //transform to cartesian coords + e.rx() -= m_map.width() / 2; //should be an int + e.ry() = m_map.height() / 2 - e.y(); + + double length = hypot( e.x(), e.y() ); + + if( length >= m_map.m_innerRadius ) //not hovering over inner circle + { + uint depth = ((int)length - m_map.m_innerRadius) / m_map.m_ringBreadth; + + if( depth <= m_map.m_visibleDepth ) //**** do earlier since you can //** check not outside of range + { + //vector calculation, reduces to simple trigonometry + //cos angle = (aibi + ajbj) / albl + //ai = x, bi=1, aj=y, bj=0 + //cos angle = x / (length) + + uint a = (uint)(acos( (double)e.x() / length ) * 916.736); //916.7324722 = #radians in circle * 16 + + //acos only understands 0-180 degrees + if( e.y() < 0 ) a = 5760 - a; + + #define ring (m_map.m_signature + depth) + for( ConstIterator<Segment> it = ring->constIterator(); it != ring->end(); ++it ) + if( (*it)->intersects( a ) ) + return *it; + #undef ring + } + } + else return m_rootSegment; //hovering over inner circle + } + + return 0; +} + +void +RadialMap::Widget::mouseMoveEvent( QMouseEvent *e ) +{ + //set m_focus to what we hover over, update UI if it's a new segment + + Segment const * const oldFocus = m_focus; + QPoint p = e->pos(); + + m_focus = segmentAt( p ); //NOTE p is passed by non-const reference + + if( m_focus && m_focus->file() != m_tree ) + { + if( m_focus != oldFocus ) //if not same as last time + { + setCursor( KCursor::handCursor() ); + m_tip->updateTip( m_focus->file(), m_tree ); + emit mouseHover( m_focus->file()->fullPath() ); + + //repaint required to update labels now before transparency is generated + repaint( false ); + } + + m_tip->moveTo( e->globalPos(), *this, ( p.y() < 0 ) ); //updates tooltip psuedo-tranparent background + } + else if( oldFocus && oldFocus->file() != m_tree ) + { + unsetCursor(); + m_tip->hide(); + update(); + + emit mouseHover( QString::null ); + } +} + +void +RadialMap::Widget::mousePressEvent( QMouseEvent *e ) +{ + //m_tip is hidden already by event filter + //m_focus is set correctly (I've been strict, I assure you it is correct!) + + enum { Konqueror, Konsole, Center, Open, Copy, Delete }; + + if (m_focus && !m_focus->isFake()) + { + const KURL url = Widget::url( m_focus->file() ); + const bool isDir = m_focus->file()->isDirectory(); + + if( e->button() == Qt::RightButton ) + { + KPopupMenu popup; + popup.insertTitle( m_focus->file()->fullPath( m_tree ) ); + + if (isDir) { + popup.insertItem( SmallIconSet( "konqueror" ), i18n( "Open &Konqueror Here" ), Konqueror ); + + if( url.protocol() == "file" ) + popup.insertItem( SmallIconSet( "konsole" ), i18n( "Open &Konsole Here" ), Konsole ); + + if (m_focus->file() != m_tree) { + popup.insertSeparator(); + popup.insertItem( SmallIconSet( "viewmag" ), i18n( "&Center Map Here" ), Center ); + } + } + else + popup.insertItem( SmallIconSet( "fileopen" ), i18n( "&Open" ), Open ); + + popup.insertSeparator(); + popup.insertItem( SmallIconSet( "editcopy" ), i18n( "&Copy to clipboard" ), Copy ); + + popup.insertSeparator(); + popup.insertItem( SmallIconSet( "editdelete" ), i18n( "&Delete" ), Delete ); + + switch (popup.exec( e->globalPos(), 1 )) { + case Konqueror: + //KRun::runCommand will show an error message if there was trouble + KRun::runCommand( QString( "kfmclient openURL \"%1\"" ).arg( url.url() ) ); + break; + + case Konsole: + // --workdir only works for local file paths + KRun::runCommand( QString( "konsole --workdir \"%1\"" ).arg( url.path() ) ); + break; + + case Center: + case Open: + goto section_two; + + case Copy: + QApplication::clipboard()->setData( new KURLDrag( KURL::List( url ) ) ); + break; + + case Delete: + { + const KURL url = Widget::url( m_focus->file() ); + const QString message = m_focus->file()->isDirectory() + ? i18n( "<qt>The directory at <i>'%1'</i> will be <b>recursively</b> and <b>permanently</b> deleted." ) + : i18n( "<qt><i>'%1'</i> will be <b>permanently</b> deleted." ); + const int userIntention = KMessageBox::warningContinueCancel( + this, message.arg( url.prettyURL() ), + QString::null, KGuiItem( i18n("&Delete"), "editdelete" ) ); + + if (userIntention == KMessageBox::Continue) { + KIO::Job *job = KIO::del( url ); + job->setWindow( this ); + connect( job, SIGNAL(result( KIO::Job* )), SLOT(deleteJobFinished( KIO::Job* )) ); + QApplication::setOverrideCursor( KCursor::workingCursor() ); + } + } + + default: + //ensure m_focus is set for new mouse position + sendFakeMouseEvent(); + } + } + else { // not right mouse button + + section_two: + const QRect rect( e->x() - 20, e->y() - 20, 40, 40 ); + + m_tip->hide(); // user expects this + + if (!isDir || e->button() == Qt::MidButton) { + KIconEffect::visualActivate( this, rect ); + new KRun( url, this, true ); //FIXME see above + } + else if (m_focus->file() != m_tree) { // is left click + KIconEffect::visualActivate( this, rect ); + emit activated( url ); //activate first, this will cause UI to prepare itself + createFromCache( (Directory *)m_focus->file() ); + } + else + emit giveMeTreeFor( url.upURL() ); + } + } +} + +void +RadialMap::Widget::deleteJobFinished( KIO::Job *job ) +{ + QApplication::restoreOverrideCursor(); + if( !job->error() ) + invalidate(); + else + job->showErrorDialog( this ); +} + +#include "debug.h" +void +RadialMap::Widget::dropEvent( QDropEvent *e ) +{ + DEBUG_ANNOUNCE + + KURL::List urls; + if (KURLDrag::decode( e, urls ) && urls.count()) + emit giveMeTreeFor( urls.first() ); +} + +void +RadialMap::Widget::dragEnterEvent( QDragEnterEvent *e ) +{ + DEBUG_ANNOUNCE + + e->accept( KURLDrag::canDecode( e ) ); +} diff --git a/src/part/remoteLister.cpp b/src/part/remoteLister.cpp new file mode 100644 index 0000000..dcfbbee --- /dev/null +++ b/src/part/remoteLister.cpp @@ -0,0 +1,160 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include <debug.h> +#include "fileTree.h" +#include <qapplication.h> +#include <qtimer.h> +#include <qvaluelist.h> +#include "remoteLister.h" +#include "scan.h" + +namespace Filelight +{ + //you need to use a single DirLister + //one per directory breaks KIO (seemingly) and also uses un-godly amounts of memory! + + //TODO delete all this stuff! + + struct Store { + + typedef QValueList<Store*> List; + + /// location of the directory + const KURL url; + /// the directory on which we are operating + Directory *directory; + /// so we can reference the parent store + Store *parent; + /// directories in this directory that need to be scanned before we can propagate() + List stores; + + Store() + : directory( 0 ), parent( 0 ) {} + Store( const KURL &u, const QString &name, Store *s ) + : url( u ), directory( new Directory( name.local8Bit() + '/' ) ), parent( s ) {} + + + Store* + propagate() + { + /// returns the next store available for scanning + + debug() << "propagate: " << url << endl; + + if( parent ) { + parent->directory->append( directory ); + if( parent->stores.isEmpty() ) { + return parent->propagate(); + } + else + return parent; + } + + //we reached the root, let's get our next directory scanned + return this; + } + + private: + Store( Store& ); + Store &operator=( const Store& ); + }; + + + RemoteLister::RemoteLister( const KURL &url, QWidget *parent ) + : KDirLister( true /*don't fetch mimetypes*/ ) + , m_root( new Store( url, url.url(), 0 ) ) + , m_store( m_root ) + { + setAutoUpdate( false ); //don't use KDirWatchers + setShowingDotFiles( true ); //stupid KDirLister API function names + setMainWindow( parent ); + + //use SIGNAL(result(KIO::Job*)) instead and then use Job::error() + connect( this, SIGNAL(completed()), SLOT(completed()) ); + connect( this, SIGNAL(canceled()), SLOT(canceled()) ); + + //we do this non-recursively - it is the only way! + openURL( url ); + } + + RemoteLister::~RemoteLister() + { + Directory *tree = isFinished() ? m_store->directory : 0; + + QCustomEvent *e = new QCustomEvent( 1000 ); + e->setData( tree ); + QApplication::postEvent( parent(), e ); + + delete m_root; + } + + void + RemoteLister::completed() + { + debug() << "completed: " << url().prettyURL() << endl; + + //as usual KDE documentation didn't suggest I needed to do this at all + //I had to figure it out myself + // -- avoid crash + QTimer::singleShot( 0, this, SLOT(_completed()) ); + } + + void + RemoteLister::canceled() + { + debug() << "canceled: " << url().prettyURL() << endl; + + QTimer::singleShot( 0, this, SLOT(_completed()) ); + } + + void + RemoteLister::_completed() + { + //m_directory is set to the directory we should operate on + + KFileItemList items = KDirLister::items(); + for( KFileItemList::ConstIterator it = items.begin(), end = items.end(); it != end; ++it ) + { + if( (*it)->isDir() ) + m_store->stores += new Store( (*it)->url(), (*it)->name(), m_store ); + else + m_store->directory->append( (*it)->name().local8Bit(), (*it)->size() / 1024 ); + + ScanManager::s_files++; + } + + + if( m_store->stores.isEmpty() ) + //no directories to scan, so we need to append ourselves to the parent directory + //propagate() will return the next ancestor that has stores left to be scanned, or root if we are done + m_store = m_store->propagate(); + + if( !m_store->stores.isEmpty() ) + { + Store::List::Iterator first = m_store->stores.begin(); + const KURL url( (*first)->url ); + Store *currentStore = m_store; + + //we should operate with this store next time this function is called + m_store = *first; + + //we don't want to handle this store again + currentStore->stores.remove( first ); + + //this returns _immediately_ + debug() << "scanning: " << url << endl; + openURL( url ); + } + else { + + debug() << "I think we're done\n"; + + Q_ASSERT( m_root == m_store ); + + delete this; + } + } +} + +#include "remoteLister.moc" diff --git a/src/part/remoteLister.h b/src/part/remoteLister.h new file mode 100644 index 0000000..4736dc1 --- /dev/null +++ b/src/part/remoteLister.h @@ -0,0 +1,28 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#ifndef REMOTELISTER_H +#define REMOTELISTER_H + +#include <kdirlister.h> + +namespace Filelight +{ + class RemoteLister : public KDirLister + { + Q_OBJECT + public: + RemoteLister( const KURL &url, QWidget *parent ); + ~RemoteLister(); + + private slots: + void completed(); + void _completed(); + void canceled(); + + private: + class Store *m_root, *m_store; + }; +} + +#endif diff --git a/src/part/scan.cpp b/src/part/scan.cpp new file mode 100644 index 0000000..2101624 --- /dev/null +++ b/src/part/scan.cpp @@ -0,0 +1,204 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include "debug.h" +#include "fileTree.h" +#include <kcursor.h> +#include "localLister.h" +#include <qapplication.h> +#include "remoteLister.h" +#include "scan.h" + + +namespace Filelight +{ + bool ScanManager::s_abort = false; + uint ScanManager::s_files = 0; + + ScanManager::ScanManager( QObject *parent ) + : QObject( parent ) + , m_thread( 0 ) + , m_cache( new Chain<Directory> ) + { + Filelight::LocalLister::readMounts(); + } + + ScanManager::~ScanManager() + { + if( m_thread ) { + debug() << "Attempting to abort scan operation...\n"; + s_abort = true; + m_thread->wait(); + } + + delete m_cache; + + //RemoteListers are QObjects and get automatically deleted + } + + bool + ScanManager::running() const + { + //FIXME not complete + return m_thread && m_thread->running(); + } + + bool + ScanManager::start( const KURL &url ) + { + //url is guarenteed clean and safe + + debug() << "Scan requested for: " << url.prettyURL() << endl; + + if( running() ) { + //shouldn't happen, but lets prevent mega-disasters just in case eh? + kdWarning() << "Attempted to run 2 scans concurrently!\n"; + //TODO give user an error + return false; + } + + s_files = 0; + s_abort = false; + + if( url.protocol() == "file" ) + { + const QString path = url.path( 1 ); + + Chain<Directory> *trees = new Chain<Directory>; + + /* CHECK CACHE + * user wants: /usr/local/ + * cached: /usr/ + * + * user wants: /usr/ + * cached: /usr/local/, /usr/include/ + */ + + for( Iterator<Directory> it = m_cache->iterator(); it != m_cache->end(); ++it ) + { + QString cachePath = (*it)->name(); + + if( path.startsWith( cachePath ) ) //then whole tree already scanned + { + //find a pointer to the requested branch + + debug() << "Cache-(a)hit: " << cachePath << endl; + + QStringList split = QStringList::split( '/', path.mid( cachePath.length() ) ); + Directory *d = *it; + Iterator<File> jt; + + while( !split.isEmpty() && d != NULL ) //if NULL we have got lost so abort!! + { + jt = d->iterator(); + + const Link<File> *end = d->end(); + QString s = split.first(); s += '/'; + + for( d = 0; jt != end; ++jt ) + if( s == (*jt)->name() ) + { + d = (Directory*)*jt; + break; + } + + split.pop_front(); + } + + if( d ) + { + delete trees; + + //we found a completed tree, thus no need to scan + debug() << "Found cache-handle, generating map..\n"; + + //1001 indicates that this should not be cached + QCustomEvent *e = new QCustomEvent( 1001 ); + e->setData( d ); + QApplication::postEvent( this, e ); + + return true; + } + else + { + //something went wrong, we couldn't find the directory we were expecting + error() << "Didn't find " << path << " in the cache!\n"; + delete it.remove(); //safest to get rid of it + break; //do a full scan + } + } + else if( cachePath.startsWith( path ) ) //then part of the requested tree is already scanned + { + debug() << "Cache-(b)hit: " << cachePath << endl; + it.transferTo( *trees ); + } + } + + m_url.setPath( path ); //FIXME stop switching between paths and KURLs all the time + QApplication::setOverrideCursor( KCursor::workingCursor() ); + //starts listing by itself + m_thread = new Filelight::LocalLister( path, trees, this ); + return true; + } + + m_url = url; + QApplication::setOverrideCursor( KCursor::workingCursor() ); + //will start listing straight away + QObject *o = new Filelight::RemoteLister( url, (QWidget*)parent() ); + insertChild( o ); + o->setName( "remote_lister" ); + return true; + } + + bool + ScanManager::abort() + { + s_abort = true; + + delete child( "remote_lister" ); + + return m_thread && m_thread->running(); + } + + void + ScanManager::emptyCache() + { + s_abort = true; + + if( m_thread && m_thread->running() ) + m_thread->wait(); + + emit aboutToEmptyCache(); + + m_cache->empty(); + } + + void + ScanManager::customEvent( QCustomEvent *e ) + { + Directory *tree = (Directory*)e->data(); + + if( m_thread ) { + m_thread->terminate(); + m_thread->wait(); + delete m_thread; //note the lister deletes itself + m_thread = 0; + } + + emit completed( tree ); + + if( tree ) { + //we don't cache foreign stuff + //we don't recache stuff (thus only type 1000 events) + if( e->type() == 1000 && m_url.protocol() == "file" ) + //TODO sanity check the cache + m_cache->append( tree ); + } + else //scan failed + m_cache->empty(); //FIXME this is safe but annoying + + QApplication::restoreOverrideCursor(); + } +} + +#include "scan.moc" diff --git a/src/part/scan.h b/src/part/scan.h new file mode 100644 index 0000000..c02b1d2 --- /dev/null +++ b/src/part/scan.h @@ -0,0 +1,52 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#ifndef SCAN_H +#define SCAN_H + +#include <kurl.h> +#include <qobject.h> + +class QThread; +class Directory; +template<class T> class Chain; + +namespace Filelight +{ + class ScanManager : public QObject + { + Q_OBJECT + + friend class LocalLister; + friend class RemoteLister; + + public: + ScanManager( QObject *parent ); + virtual ~ScanManager(); + + bool start( const KURL& ); + bool running() const; + + static uint files() { return s_files; } + + public slots: + bool abort(); + void emptyCache(); + + signals: + void completed( Directory* ); + void aboutToEmptyCache(); + + private: + static bool s_abort; + static uint s_files; + + KURL m_url; + QThread *m_thread; + Chain<Directory> *m_cache; + + virtual void customEvent( QCustomEvent* ); + }; +} + +#endif diff --git a/src/part/settingsDialog.cpp b/src/part/settingsDialog.cpp new file mode 100644 index 0000000..508904a --- /dev/null +++ b/src/part/settingsDialog.cpp @@ -0,0 +1,214 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#include <qapplication.h> //Getting desktop width +#include <qcheckbox.h> +#include <qpushbutton.h> +#include <qradiobutton.h> +#include <qslider.h> +#include <qvbuttongroup.h> + +#include <kdirselectdialog.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <knuminput.h> + +#include "settingsDialog.h" +#include "Config.h" + + +SettingsDialog::SettingsDialog( QWidget *parent, const char *name ) + : Dialog( parent, name, false ) //3rd param => modal +{ + colourSchemeGroup->setFrameShape( QFrame::NoFrame ); + + colourSchemeGroup->insert( new QRadioButton( i18n("Rainbow"), colourSchemeGroup ), Filelight::Rainbow ); + colourSchemeGroup->insert( new QRadioButton( i18n("KDE Colors"), colourSchemeGroup ), Filelight::KDE ); + colourSchemeGroup->insert( new QRadioButton( i18n("High Contrast"), colourSchemeGroup ), Filelight::HighContrast ); + + //read in settings before you make all those nasty connections! + reset(); //makes dialog reflect global settings + + connect( &m_timer, SIGNAL(timeout()), SIGNAL(mapIsInvalid()) ); + + connect( m_addButton, SIGNAL( clicked() ), SLOT( addDirectory() ) ); + connect( m_removeButton, SIGNAL( clicked() ), SLOT( removeDirectory() ) ); + connect( m_resetButton, SIGNAL( clicked() ), SLOT( reset() ) ); + connect( m_closeButton, SIGNAL( clicked() ), SLOT( close() ) ); + + connect( colourSchemeGroup, SIGNAL(clicked( int )), SLOT(changeScheme( int )) ); + connect( contrastSlider, SIGNAL(valueChanged( int )), SLOT(changeContrast( int )) ); + connect( contrastSlider, SIGNAL(sliderReleased()), SLOT(slotSliderReleased()) ); + + connect( scanAcrossMounts, SIGNAL( toggled( bool ) ), SLOT( startTimer() ) ); + connect( dontScanRemoteMounts, SIGNAL( toggled( bool ) ), SLOT( startTimer() ) ); + connect( dontScanRemovableMedia, SIGNAL( toggled( bool ) ), SLOT( startTimer() ) ); + + connect( useAntialiasing, SIGNAL( toggled( bool ) ), SLOT( toggleUseAntialiasing( bool ) ) ); + connect( varyLabelFontSizes, SIGNAL( toggled( bool ) ), SLOT( toggleVaryLabelFontSizes( bool ) ) ); + connect( showSmallFiles, SIGNAL( toggled( bool ) ), SLOT( toggleShowSmallFiles( bool ) ) ); + + connect( minFontPitch, SIGNAL ( valueChanged( int ) ), SLOT( changeMinFontPitch( int ) ) ); + + m_addButton->setIconSet( SmallIcon( "fileopen" ) ); + m_resetButton->setIconSet( SmallIcon( "undo" ) ); + m_closeButton->setIconSet( SmallIcon( "fileclose" ) ); +} + + +void SettingsDialog::closeEvent( QCloseEvent* ) +{ + //if an invalidation is pending, force it now! + if( m_timer.isActive() ) m_timer.changeInterval( 0 ); + + Config::write(); + + deleteLater(); +} + + +void SettingsDialog::reset() +{ + Config::read(); + + //tab 1 + scanAcrossMounts->setChecked( Config::scanAcrossMounts ); + dontScanRemoteMounts->setChecked( !Config::scanRemoteMounts ); + dontScanRemovableMedia->setChecked( !Config::scanRemovableMedia ); + + dontScanRemoteMounts->setEnabled( Config::scanAcrossMounts ); + // dontScanRemovableMedia.setEnabled( Config::scanAcrossMounts ); + + m_listBox->clear(); + m_listBox->insertStringList( Config::skipList ); + m_listBox->setSelected( 0, true ); + + m_removeButton->setEnabled( m_listBox->count() == 0 ); + + //tab 2 + if( colourSchemeGroup->id( colourSchemeGroup->selected() ) != Config::scheme ) + { + colourSchemeGroup->setButton( Config::scheme ); + //setButton doesn't call a single QButtonGroup signal! + //so we need to call this ourselves (and hence the detection above) + changeScheme( Config::scheme ); + } + contrastSlider->setValue( Config::contrast ); + + useAntialiasing->setChecked( (Config::antiAliasFactor > 1) ? true : false ); + + varyLabelFontSizes->setChecked( Config::varyLabelFontSizes ); + minFontPitch->setEnabled( Config::varyLabelFontSizes ); + minFontPitch->setValue( Config::minFontPitch ); + showSmallFiles->setChecked( Config::showSmallFiles ); +} + + + +void SettingsDialog::toggleScanAcrossMounts( bool b ) +{ + Config::scanAcrossMounts = b; + + dontScanRemoteMounts->setEnabled( b ); + //dontScanRemovableMedia.setEnabled( b ); +} + +void SettingsDialog::toggleDontScanRemoteMounts( bool b ) +{ + Config::scanRemoteMounts = !b; +} + +void SettingsDialog::toggleDontScanRemovableMedia( bool b ) +{ + Config::scanRemovableMedia = !b; +} + + + +void SettingsDialog::addDirectory() +{ + const KURL url = KDirSelectDialog::selectDirectory( "/", false, this ); + + //TODO error handling! + //TODO wrong protocol handling! + + if( !url.isEmpty() ) + { + const QString path = url.path( 1 ); + + if( !Config::skipList.contains( path ) ) + { + Config::skipList.append( path ); + m_listBox->insertItem( path ); + m_removeButton->setEnabled( true ); + } + else KMessageBox::sorry( this, i18n("That directory is already set to be excluded from scans") ); + } +} + + +void SettingsDialog::removeDirectory() +{ + Config::skipList.remove( m_listBox->currentText() ); //removes all entries that match + + //safest method to ensure consistency + m_listBox->clear(); + m_listBox->insertStringList( Config::skipList ); + + m_removeButton->setEnabled( m_listBox->count() == 0 ); +} + + +void SettingsDialog::startTimer() +{ + m_timer.start( TIMEOUT, true ); +} + +void SettingsDialog::changeScheme( int s ) +{ + Config::scheme = (Filelight::MapScheme)s; + emit canvasIsDirty( 1 ); +} +void SettingsDialog::changeContrast( int c ) +{ + Config::contrast = c; + emit canvasIsDirty( 3 ); +} +void SettingsDialog::toggleUseAntialiasing( bool b ) +{ + Config::antiAliasFactor = b ? 2 : 1; + emit canvasIsDirty( 2 ); +} +void SettingsDialog::toggleVaryLabelFontSizes( bool b ) +{ + Config::varyLabelFontSizes = b; + minFontPitch->setEnabled( b ); + emit canvasIsDirty( 0 ); +} +void SettingsDialog::changeMinFontPitch( int p ) +{ + Config::minFontPitch = p; + emit canvasIsDirty( 0 ); +} +void SettingsDialog::toggleShowSmallFiles( bool b ) +{ + Config::showSmallFiles = b; + emit canvasIsDirty( 1 ); +} + + +void SettingsDialog::slotSliderReleased() +{ + emit canvasIsDirty( 2 ); +} + + +void SettingsDialog::reject() +{ + //called when escape is pressed + reset(); + QDialog::reject(); //**** doesn't change back scheme so far +} + +#include "settingsDialog.moc" diff --git a/src/part/settingsDialog.h b/src/part/settingsDialog.h new file mode 100644 index 0000000..b3ed375 --- /dev/null +++ b/src/part/settingsDialog.h @@ -0,0 +1,48 @@ +//Author: Max Howell <[email protected]>, (C) 2003-4 +//Copyright: See COPYING file that comes with this distribution + +#ifndef SETTINGSDLG_H +#define SETTINGSDLG_H + +#include "dialog.h" //generated by uic +#include <qtimer.h> + + +class SettingsDialog : public Dialog +{ +Q_OBJECT + +public: + SettingsDialog( QWidget* =0, const char* =0 ); + +protected: + virtual void closeEvent( QCloseEvent * ); + virtual void reject(); + +public slots: + void addDirectory(); + void removeDirectory(); + void toggleScanAcrossMounts( bool ); + void toggleDontScanRemoteMounts( bool ); + void toggleDontScanRemovableMedia( bool ); + void reset(); + void startTimer(); + void toggleUseAntialiasing( bool = true ); + void toggleVaryLabelFontSizes( bool ); + void changeContrast( int ); + void changeScheme( int ); + void changeMinFontPitch( int ); + void toggleShowSmallFiles( bool ); + void slotSliderReleased(); + +signals: + void mapIsInvalid(); + void canvasIsDirty( int ); + +private: + QTimer m_timer; + + static const uint TIMEOUT=1000; +}; + +#endif diff --git a/src/part/summaryWidget.cpp b/src/part/summaryWidget.cpp new file mode 100644 index 0000000..0e3a140 --- /dev/null +++ b/src/part/summaryWidget.cpp @@ -0,0 +1,236 @@ +//Author: Max Howell <[email protected]>, (C) 2004 +//Copyright: See COPYING file that comes with this distribution + +#include "Config.h" +#include "debug.h" +#include "fileTree.h" +#include <kcursor.h> +#include <kiconeffect.h> //MyRadialMap::mousePressEvent() +#include <kiconloader.h> +#include <klocale.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qtextstream.h> +#include <qvbox.h> +#include "radialMap/radialMap.h" +#include "radialMap/widget.h" +#include "summaryWidget.h" +#include "summaryWidget.moc" + + +static Filelight::MapScheme oldScheme; + + +struct Disk +{ + QString device; + QString type; + QString mount; + QString icon; + + int size; + int used; + int free; //NOTE used+avail != size (clustersize!) + + void guessIconName(); +}; + + +struct DiskList : QValueList<Disk> +{ + DiskList(); +}; + + +class MyRadialMap : public RadialMap::Widget +{ +public: + MyRadialMap( QWidget *parent ) + : RadialMap::Widget( parent ) + {} + + virtual void setCursor( const QCursor &c ) + { + if( focusSegment() && focusSegment()->file()->name() == "Used" ) + RadialMap::Widget::setCursor( c ); + else + unsetCursor(); + } + + virtual void mousePressEvent( QMouseEvent *e ) + { + const RadialMap::Segment *segment = focusSegment(); + + //we will allow right clicks to the center circle + if( segment == rootSegment() ) + RadialMap::Widget::mousePressEvent( e ); + + //and clicks to the used segment + else if( segment && segment->file()->name() == "Used" ) { + const QRect rect( e->x() - 20, e->y() - 20, 40, 40 ); + KIconEffect::visualActivate( this, rect ); + emit activated( url() ); + } + } +}; + + + +SummaryWidget::SummaryWidget( QWidget *parent, const char *name ) + : QWidget( parent, name ) +{ + qApp->setOverrideCursor( KCursor::waitCursor() ); + + setPaletteBackgroundColor( Qt::white ); + (new QGridLayout( this, 1, 2 ))->setAutoAdd( true ); + + createDiskMaps(); + + qApp->restoreOverrideCursor(); +} + +SummaryWidget::~SummaryWidget() +{ + Config::scheme = oldScheme; +} + +void +SummaryWidget::createDiskMaps() +{ + DiskList disks; + + const QCString free = i18n( "Free" ).local8Bit(); + const QCString used = i18n( "Used" ).local8Bit(); + + KIconLoader loader; + + oldScheme = Config::scheme; + Config::scheme = (Filelight::MapScheme)2000; + + for (DiskList::ConstIterator it = disks.begin(), end = disks.end(); it != end; ++it) + { + Disk const &disk = *it; + + if (disk.free == 0 && disk.used == 0) + continue; + + QWidget *box = new QVBox( this ); + RadialMap::Widget *map = new MyRadialMap( box ); + + QString text; QTextOStream( &text ) + << "<img src='" << loader.iconPath( disk.icon, KIcon::Toolbar ) << "'>" + << " " << disk.mount << " " + << "<i>(" << disk.device << ")</i>"; + + QLabel *label = new QLabel( text, box ); + label->setAlignment( Qt::AlignCenter ); + label->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Maximum ); + + box->show(); // will show its children too + + Directory *tree = new Directory( disk.mount.local8Bit() ); + tree->append( free, disk.free ); + tree->append( used, disk.used ); + + map->create( tree ); //must be done when visible + + connect( map, SIGNAL(activated( const KURL& )), SIGNAL(activated( const KURL& )) ); + } +} + + +#if defined(_OS_LINUX_) +#define DF_ARGS "-kT" +#else +#define DF_ARGS "-k" +#define NO_FS_TYPE +#endif + + +DiskList::DiskList() +{ + //FIXME bug prone + setenv( "LANG", "en_US", 1 ); + setenv( "LC_ALL", "en_US", 1 ); + setenv( "LC_MESSAGES", "en_US", 1 ); + setenv( "LC_TYPE", "en_US", 1 ); + setenv( "LANGUAGE", "en_US", 1 ); + + char buffer[4096]; + FILE *df = popen( "env LC_ALL=POSIX df " DF_ARGS, "r" ); + int const N = fread( (void*)buffer, sizeof(char), 4096, df ); + buffer[ N ] = '\0'; + pclose( df ); + + QString output = QString::fromLocal8Bit( buffer ); + QTextStream t( &output, IO_ReadOnly ); + QString const BLANK( QChar(' ') ); + + while (!t.atEnd()) { + QString s = t.readLine(); + s = s.simplifyWhiteSpace(); + + if (s.isEmpty()) + continue; + + if (s.find( BLANK ) < 0) // devicename was too long, rest in next line + if (!t.eof()) { // just appends the next line + QString v = t.readLine(); + s = s.append( v.latin1() ); + s = s.simplifyWhiteSpace(); + } + + Disk disk; + disk.device = s.left( s.find( BLANK ) ); + s = s.remove( 0, s.find( BLANK ) + 1 ); + + #ifndef NO_FS_TYPE + disk.type = s.left( s.find( BLANK ) ); + s = s.remove( 0, s.find( BLANK ) + 1 ); + #endif + + int n = s.find( BLANK ); + disk.size = s.left( n ).toInt(); + s = s.remove( 0, n + 1 ); + + n = s.find( BLANK ); + disk.used = s.left( n ).toInt(); + s = s.remove( 0, n + 1 ); + + n = s.find( BLANK ); + disk.free = s.left( n ).toInt(); + s = s.remove( 0, n + 1 ); + + s = s.remove( 0, s.find( BLANK ) + 1 ); // delete the capacity 94% + disk.mount = s; + + disk.guessIconName(); + + *this += disk; + } +} + + +void +Disk::guessIconName() +{ + if( mount.contains( "cdrom", false ) ) icon = "cdrom"; + else if( device.contains( "cdrom", false ) ) icon = "cdrom"; + else if( mount.contains( "writer", false ) ) icon = "cdwriter"; + else if( device.contains( "writer", false ) ) icon = "cdwriter"; + else if( mount.contains( "mo", false ) ) icon = "mo"; + else if( device.contains( "mo", false ) ) icon = "mo"; + else if( device.contains( "fd", false ) ) { + if( device.contains( "360", false ) ) icon = "5floppy"; + if( device.contains( "1200", false ) ) icon = "5floppy"; + else + icon = "3floppy"; + } + else if( mount.contains( "floppy", false ) ) icon = "3floppy"; + else if( mount.contains( "zip", false ) ) icon = "zip"; + else if( type.contains( "nfs", false ) ) icon = "nfs"; + else + icon = "hdd"; + + icon += /*mounted() ? */"_mount"/* : "_unmount"*/; +} diff --git a/src/part/summaryWidget.h b/src/part/summaryWidget.h new file mode 100644 index 0000000..7f20153 --- /dev/null +++ b/src/part/summaryWidget.h @@ -0,0 +1,25 @@ +//Author: Max Howell <[email protected]>, (C) 2004 +//Copyright: See COPYING file that comes with this distribution + +#ifndef FILELIGHTSUMMARY_H +#define FILELIGHTSUMMARY_H + +#include <qwidget.h> + + +class SummaryWidget : public QWidget +{ + Q_OBJECT + +public: + SummaryWidget( QWidget *parent, const char *name ); + ~SummaryWidget(); + +signals: + void activated( const KURL& ); + +private: + void createDiskMaps(); +}; + +#endif |