/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include "XTempFile.hxx" #include #include #include #include #include #include #include #include OTempFileService::OTempFileService(css::uno::Reference< css::uno::XComponentContext > const &) : mpStream( nullptr ) , mbRemoveFile( true ) , mbInClosed( false ) , mbOutClosed( false ) , mnCachedPos( 0 ) , mbHasCachedPos( false ) { mpTempFile.reset(new utl::TempFile()); mpTempFile->EnableKillingFile(); } OTempFileService::~OTempFileService () { } // XTypeProvider css::uno::Sequence< css::uno::Type > SAL_CALL OTempFileService::getTypes( ) { static ::cppu::OTypeCollection ourTypeCollection( cppu::UnoType::get() ,OTempFileBase::getTypes() ); return ourTypeCollection.getTypes(); }; // XTempFile sal_Bool SAL_CALL OTempFileService::getRemoveFile() { ::osl::MutexGuard aGuard( maMutex ); if ( !mpTempFile ) { // the stream is already disconnected throw css::uno::RuntimeException(); } return mbRemoveFile; }; void SAL_CALL OTempFileService::setRemoveFile( sal_Bool _removefile ) { ::osl::MutexGuard aGuard( maMutex ); if ( !mpTempFile ) { // the stream is already disconnected throw css::uno::RuntimeException(); } mbRemoveFile = _removefile; mpTempFile->EnableKillingFile( mbRemoveFile ); }; OUString SAL_CALL OTempFileService::getUri() { ::osl::MutexGuard aGuard( maMutex ); if ( !mpTempFile ) { throw css::uno::RuntimeException(); } return mpTempFile->GetURL(); }; OUString SAL_CALL OTempFileService::getResourceName() { ::osl::MutexGuard aGuard( maMutex ); if ( !mpTempFile ) { throw css::uno::RuntimeException(); } return mpTempFile->GetFileName(); }; // XInputStream sal_Int32 SAL_CALL OTempFileService::readBytes( css::uno::Sequence< sal_Int8 >& aData, sal_Int32 nBytesToRead ) { ::osl::MutexGuard aGuard( maMutex ); if ( mbInClosed ) throw css::io::NotConnectedException ( OUString(), static_cast < css::uno::XWeak * > (this ) ); checkConnected(); if (nBytesToRead < 0) throw css::io::BufferSizeExceededException( OUString(), static_cast< css::uno::XWeak * >(this)); if (aData.getLength() < nBytesToRead) aData.realloc(nBytesToRead); sal_uInt32 nRead = mpStream->ReadBytes(static_cast(aData.getArray()), nBytesToRead); checkError(); if (nRead < static_cast(aData.getLength())) aData.realloc( nRead ); if ( sal::static_int_cast(nBytesToRead) > nRead ) { // usually that means that the stream was read till the end // TODO/LATER: it is better to get rid of this optimization by avoiding using of multiple temporary files ( there should be only one temporary file? ) mnCachedPos = mpStream->Tell(); mbHasCachedPos = true; mpStream = nullptr; if ( mpTempFile ) mpTempFile->CloseStream(); } return nRead; } sal_Int32 SAL_CALL OTempFileService::readSomeBytes( css::uno::Sequence< sal_Int8 >& aData, sal_Int32 nMaxBytesToRead ) { ::osl::MutexGuard aGuard( maMutex ); if ( mbInClosed ) throw css::io::NotConnectedException ( OUString(), static_cast < css::uno::XWeak * > (this ) ); checkConnected(); checkError(); if (nMaxBytesToRead < 0) throw css::io::BufferSizeExceededException( OUString(), static_cast < css::uno::XWeak * >( this ) ); if (mpStream->eof()) { aData.realloc(0); return 0; } else return readBytes(aData, nMaxBytesToRead); } void SAL_CALL OTempFileService::skipBytes( sal_Int32 nBytesToSkip ) { ::osl::MutexGuard aGuard( maMutex ); if ( mbInClosed ) throw css::io::NotConnectedException ( OUString(), static_cast < css::uno::XWeak * > (this ) ); checkConnected(); checkError(); mpStream->SeekRel(nBytesToSkip); checkError(); } sal_Int32 SAL_CALL OTempFileService::available( ) { ::osl::MutexGuard aGuard( maMutex ); if ( mbInClosed ) throw css::io::NotConnectedException ( OUString(), static_cast < css::uno::XWeak * > (this ) ); checkConnected(); sal_Int64 nAvailable = mpStream->remainingSize(); checkError(); return std::min(SAL_MAX_INT32, nAvailable); } void SAL_CALL OTempFileService::closeInput( ) { ::osl::MutexGuard aGuard( maMutex ); if ( mbInClosed ) throw css::io::NotConnectedException ( OUString(), static_cast < css::uno::XWeak * > (this ) ); mbInClosed = true; if ( mbOutClosed ) { // stream will be deleted by TempFile implementation mpStream = nullptr; mpTempFile.reset(); } } // XOutputStream void SAL_CALL OTempFileService::writeBytes( const css::uno::Sequence< sal_Int8 >& aData ) { ::osl::MutexGuard aGuard( maMutex ); if ( mbOutClosed ) throw css::io::NotConnectedException ( OUString(), static_cast < css::uno::XWeak * > (this ) ); checkConnected(); sal_uInt32 nWritten = mpStream->WriteBytes(aData.getConstArray(), aData.getLength()); checkError(); if ( nWritten != static_cast(aData.getLength())) throw css::io::BufferSizeExceededException( OUString(),static_cast < css::uno::XWeak * > ( this ) ); } void SAL_CALL OTempFileService::flush( ) { ::osl::MutexGuard aGuard( maMutex ); if ( mbOutClosed ) throw css::io::NotConnectedException ( OUString(), static_cast < css::uno::XWeak * > (this ) ); checkConnected(); mpStream->Flush(); checkError(); } void SAL_CALL OTempFileService::closeOutput( ) { ::osl::MutexGuard aGuard( maMutex ); if ( mbOutClosed ) throw css::io::NotConnectedException ( OUString(), static_cast < css::uno::XWeak * > (this ) ); mbOutClosed = true; // TODO/LATER: it is better to get rid of this optimization by avoiding using of multiple temporary files ( there should be only one temporary file? ) if ( mpStream ) { mnCachedPos = mpStream->Tell(); mbHasCachedPos = true; mpStream = nullptr; if ( mpTempFile ) mpTempFile->CloseStream(); } if ( mbInClosed ) { // stream will be deleted by TempFile implementation mpStream = nullptr; mpTempFile.reset(); } } void OTempFileService::checkError () const { if (!mpStream || mpStream->SvStream::GetError () != ERRCODE_NONE ) throw css::io::NotConnectedException ( OUString(), const_cast < css::uno::XWeak * > ( static_cast < const css::uno::XWeak * > (this ) ) ); } void OTempFileService::checkConnected () { if (!mpStream && mpTempFile) { mpStream = mpTempFile->GetStream( StreamMode::STD_READWRITE ); if ( mpStream && mbHasCachedPos ) { mpStream->Seek( sal::static_int_cast(mnCachedPos) ); if ( mpStream->SvStream::GetError () == ERRCODE_NONE ) { mbHasCachedPos = false; mnCachedPos = 0; } else { mpStream = nullptr; mpTempFile->CloseStream(); } } } if (!mpStream) throw css::io::NotConnectedException ( OUString(), static_cast < css::uno::XWeak * > (this ) ); } // XSeekable void SAL_CALL OTempFileService::seek( sal_Int64 nLocation ) { ::osl::MutexGuard aGuard( maMutex ); checkConnected(); if ( nLocation < 0 || nLocation > getLength() ) throw css::lang::IllegalArgumentException(); mpStream->Seek(static_cast(nLocation) ); checkError(); } sal_Int64 SAL_CALL OTempFileService::getPosition( ) { ::osl::MutexGuard aGuard( maMutex ); checkConnected(); sal_uInt32 nPos = mpStream->Tell(); checkError(); return static_cast(nPos); } sal_Int64 SAL_CALL OTempFileService::getLength( ) { ::osl::MutexGuard aGuard( maMutex ); checkConnected(); checkError(); sal_Int64 nEndPos = mpStream->TellEnd(); return nEndPos; } // XStream css::uno::Reference< css::io::XInputStream > SAL_CALL OTempFileService::getInputStream() { return css::uno::Reference< css::io::XInputStream >( *this, css::uno::UNO_QUERY ); } css::uno::Reference< css::io::XOutputStream > SAL_CALL OTempFileService::getOutputStream() { return css::uno::Reference< css::io::XOutputStream >( *this, css::uno::UNO_QUERY ); } // XTruncate void SAL_CALL OTempFileService::truncate() { ::osl::MutexGuard aGuard( maMutex ); checkConnected(); // SetStreamSize() call does not change the position mpStream->Seek( 0 ); mpStream->SetStreamSize( 0 ); checkError(); } #define PROPERTY_HANDLE_URI 1 #define PROPERTY_HANDLE_REMOVE_FILE 2 #define PROPERTY_HANDLE_RESOURCE_NAME 3 // XPropertySet ::css::uno::Reference< ::css::beans::XPropertySetInfo > OTempFileService::getPropertySetInfo() { // Create a table that map names to index values. // attention: properties need to be sorted by name! static cppu::OPropertyArrayHelper ourPropertyInfo( { css::beans::Property( "Uri", PROPERTY_HANDLE_URI, cppu::UnoType::get(), css::beans::PropertyAttribute::READONLY ), css::beans::Property( "RemoveFile", PROPERTY_HANDLE_REMOVE_FILE, cppu::UnoType::get(), 0 ), css::beans::Property( "ResourceName", PROPERTY_HANDLE_RESOURCE_NAME, cppu::UnoType::get(), css::beans::PropertyAttribute::READONLY ) }, true ); static css::uno::Reference< css::beans::XPropertySetInfo > xInfo( ::cppu::OPropertySetHelper::createPropertySetInfo( ourPropertyInfo ) ); return xInfo; } void OTempFileService::setPropertyValue( const ::rtl::OUString& aPropertyName, const ::css::uno::Any& aValue ) { if ( aPropertyName == "RemoveFile" ) setRemoveFile( aValue.get() ); else { assert(false); throw css::beans::UnknownPropertyException(aPropertyName); } } ::css::uno::Any OTempFileService::getPropertyValue( const ::rtl::OUString& aPropertyName ) { if ( aPropertyName == "RemoveFile" ) return css::uno::Any(getRemoveFile()); else if ( aPropertyName == "ResourceName" ) return css::uno::Any(getResourceName()); else if ( aPropertyName == "Uri" ) return css::uno::Any(getUri()); else { assert(false); throw css::beans::UnknownPropertyException(aPropertyName); } } void OTempFileService::addPropertyChangeListener( const ::rtl::OUString& /*aPropertyName*/, const ::css::uno::Reference< ::css::beans::XPropertyChangeListener >& /*xListener*/ ) { assert(false); } void OTempFileService::removePropertyChangeListener( const ::rtl::OUString& /*aPropertyName*/, const ::css::uno::Reference< ::css::beans::XPropertyChangeListener >& /*xListener*/ ) { assert(false); } void OTempFileService::addVetoableChangeListener( const ::rtl::OUString& /*aPropertyName*/, const ::css::uno::Reference< ::css::beans::XVetoableChangeListener >& /*xListener*/ ) { assert(false); } void OTempFileService::removeVetoableChangeListener( const ::rtl::OUString& /*aPropertyName*/, const ::css::uno::Reference< ::css::beans::XVetoableChangeListener >& /*xListener*/ ) { assert(false); } // XFastPropertySet void OTempFileService::setFastPropertyValue( ::sal_Int32 nHandle, const ::css::uno::Any& aValue ) { switch (nHandle) { case PROPERTY_HANDLE_REMOVE_FILE: setRemoveFile( aValue.get() ); return; } assert(false); throw css::beans::UnknownPropertyException(OUString::number(nHandle)); } ::css::uno::Any OTempFileService::getFastPropertyValue( ::sal_Int32 nHandle ) { switch (nHandle) { case PROPERTY_HANDLE_REMOVE_FILE: return css::uno::Any(getRemoveFile()); case PROPERTY_HANDLE_RESOURCE_NAME: return css::uno::Any(getResourceName()); case PROPERTY_HANDLE_URI: return css::uno::Any(getUri()); } assert(false); throw css::beans::UnknownPropertyException(OUString::number(nHandle)); } // XPropertyAccess ::css::uno::Sequence< ::css::beans::PropertyValue > OTempFileService::getPropertyValues() { return { css::beans::PropertyValue("Uri", PROPERTY_HANDLE_URI, css::uno::Any(getUri()), css::beans::PropertyState_DEFAULT_VALUE), css::beans::PropertyValue("RemoveFile", PROPERTY_HANDLE_REMOVE_FILE, css::uno::Any(getRemoveFile()), css::beans::PropertyState_DEFAULT_VALUE), css::beans::PropertyValue("ResourceName", PROPERTY_HANDLE_RESOURCE_NAME, css::uno::Any(getResourceName()), css::beans::PropertyState_DEFAULT_VALUE) }; } void OTempFileService::setPropertyValues( const ::css::uno::Sequence< ::css::beans::PropertyValue >& aProps ) { for ( auto const & rPropVal : aProps ) setPropertyValue( rPropVal.Name, rPropVal.Value ); } namespace sdecl = ::comphelper::service_decl; sdecl::class_< OTempFileService> const OTempFileServiceImpl; const sdecl::ServiceDecl OTempFileServiceDecl( OTempFileServiceImpl, "com.sun.star.io.comp.TempFile", "com.sun.star.io.TempFile"); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ ue='feature/cib_contract57d_p1'>feature/cib_contract57d_p1 LibreOffice 核心代码仓库文档基金会
summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2022-07-14clang-tidy modernize-pass-by-value in svtoolsNoel Grandin
Change-Id: I60e7373c924a479fed72eb4f0538006e3e422004 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/137019 Tested-by: Jenkins Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2022-01-26move ToolBoxItemId into its own headerCaolán McNamara
Change-Id: I34838bee7ad27bfd60d92c26af7eb2de508686b9 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/128974 Tested-by: Jenkins Reviewed-by: Caolán McNamara <caolanm@redhat.com>
2021-11-05use more DECL_DLLPRIVATE_STATIC_LINKNoel Grandin
to avoid unnecessarily exporting symbols Change-Id: I224848cea217977088fe0643511660a7c99b7277 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/124733 Tested-by: Jenkins Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2021-08-07create comphelper::OMultiTypeInterfaceContainerHelper2 and use itNoel Grandin
based on OInterfaceContainerHelper2 which is considerably faster than the original OInterfaceContainerHelper Change-Id: I9c8b6d0e5382018824bf7188a26343703abf2d51 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/120161 Tested-by: Jenkins Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2021-05-02throw() -> noexcept, part 2/3: Automatic loplugin:noexcept rewriteStephan Bergmann
Change-Id: I076f16d0536b534abf0ced4d76051eadb4c0e033 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/114949 Tested-by: Jenkins Reviewed-by: Stephan Bergmann <sbergman@redhat.com>
2021-03-11use strong_int for item ids in vcl::ToolBoxNoel
(*) fix bug in SfxToolBoxControl::StateChanged where it was using the slot id instead of the toolbox item id (*) I left the logic in SbaTableQueryBrowser alone, but it looks suspicious, casting slot ids to toolbox ids Change-Id: Ied229164c27fb4456b0515c6fdcbd1682766a1a9 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/112186 Tested-by: Jenkins Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2020-11-19tdf#124176 use pragma once instead of include guardsRoman Kuznetsov
in /core/include/svtools/ Change-Id: Ic8614f77dfde02f4c1fc4cceccd1de4ff5a05f72 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/106006 Tested-by: Jenkins Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
2020-05-13loplugin:unusedmethodsNoel Grandin
Change-Id: Id813d95a90fdbb360dfc8670c0b55b635f633965 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/94125 Tested-by: Jenkins Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2020-04-20weld SvxFontSizeBox_ImplCaolán McNamara
Change-Id: Ied8ef61226a720acac74db78ae5852cd4e624d14 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/92562 Tested-by: Jenkins Reviewed-by: Caolán McNamara <caolanm@redhat.com>
2020-03-24weld writer navigatorCaolán McNamara
GtkToggleToolButton are much wider than vcl equivalents. Split the bottom toolbar into two toolbars. Rearrange their contents so the layout of each level visually match. Notes: Master documents have two modes, master content tree and the normal content tree. You can drag entries from the content tree into the document, drag mode drop down controls whether its a link or a copy etc that's dropped in. Documents can be dropped into the content and global trees. If outline tracking isn't active, then when content changes the tree is cleared and refilled, typically an effort is made to reselect the same entry that was previously selected. Additionally, if the amount of content didn't change an effort is made to scroll back to the location the scrollbar was at before the clear. Change-Id: I00c015145eac5b1acc3398d3c40861d830e4264a Reviewed-on: https://gerrit.libreoffice.org/c/core/+/89725 Tested-by: Jenkins Reviewed-by: Caolán McNamara <caolanm@redhat.com>
2020-03-13Revert "loplugin:constfields in svtools"Noel Grandin
This reverts commit 07c4aa4298f062cee1894b80ef82e76d5ffcf4c7. Change-Id: Ic3f639581b9aca373f82c011f15be60d117b1943 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/90476 Tested-by: Jenkins Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2020-01-06weld AreaPropertyPanelCaolán McNamara
Change-Id: I5f4c4b43067b99cd57f8ea941002481ef5977e09 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/86144 Tested-by: Jenkins Reviewed-by: Caolán McNamara <caolanm@redhat.com> Tested-by: Caolán McNamara <caolanm@redhat.com>
2019-02-05tdf#42949 Fix IWYU warnings in include/svtools/*Gabor Kelemen
Found with bin/find-unneeded-includes Only removal proposals are dealt with here. Change-Id: I937ed12f2a96943664087ddcdd035f1347e84a57 Reviewed-on: https://gerrit.libreoffice.org/67102 Tested-by: Jenkins Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
2018-10-03loplugin:constfields in svtoolsNoel Grandin
Change-Id: I91553f89d8f5ee42afa52d196bf86f8a92011850 Reviewed-on: https://gerrit.libreoffice.org/61186 Tested-by: Jenkins Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2018-06-05tdf#42949 remove unused compheler includes ..Jochen Nitschke
and fix the fallout Change-Id: I15bc5d626f4d157cbc69a87392078b41e621d14e Reviewed-on: https://gerrit.libreoffice.org/54882 Tested-by: Jenkins <ci@libreoffice.org> Reviewed-by: Miklos Vajna <vmiklos@collabora.co.uk>
2017-10-23overload std::hash for OUString and OStringNoel Grandin
no need to explicitly specify it anymore Change-Id: I6ad9259cce77201fdd75152533f5151aae83e9ec Reviewed-on: https://gerrit.libreoffice.org/43567 Tested-by: Jenkins <ci@libreoffice.org> Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2017-01-26Remove dynamic exception specificationsStephan Bergmann
...(for now, from LIBO_INTERNAL_CODE only). See the mail thread starting at <https://lists.freedesktop.org/archives/libreoffice/2017-January/076665.html> "Dynamic Exception Specifications" for details. Most changes have been done automatically by the rewriting loplugin:dynexcspec (after enabling the rewriting mode, to be committed shortly). The way it only removes exception specs from declarations if it also sees a definition, it identified some dead declarations-w/o-definitions (that have been removed manually) and some cases where a definition appeared in multiple include files (which have also been cleaned up manually). There's also been cases of macro paramters (that were used to abstract over exception specs) that have become unused now (and been removed). Furthermore, some code needed to be cleaned up manually (avmedia/source/quicktime/ and connectivity/source/drivers/kab/), as I had no configurations available that would actually build that code. Missing @throws documentation has not been applied in such manual clean-up. Change-Id: I3408691256c9b0c12bc5332de976743626e13960 Reviewed-on: https://gerrit.libreoffice.org/33574 Tested-by: Jenkins <ci@libreoffice.org> Reviewed-by: Stephan Bergmann <sbergman@redhat.com>
2016-12-04tdf#88206 replace cppu::WeakImplHelper* in svtools and svxJochen Nitschke
and remove unused implbase*.hxx and compbase*.hxx includes Change-Id: Iedf6b6dce09b5baf714a1b3394bb7fb3526cca82 Reviewed-on: https://gerrit.libreoffice.org/31594 Tested-by: Jenkins <ci@libreoffice.org> Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2016-10-21loplugin:expandablemethodds in include/svtoolsNoel Grandin
Change-Id: I679b7985861203496813782138d1cf965fbc427b Reviewed-on: https://gerrit.libreoffice.org/30107 Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk> Tested-by: Noel Grandin <noel.grandin@collabora.co.uk>
2016-10-05Remove _TYPED suffix from tools/link.hxx macrosStephan Bergmann
...which was introduced with 3ead3ad52f9bb2f9d1d6cf8dfc73a0a25e6778ed "Gradually typed Link" to distinguish the new, typed versions from the old, untyped ones, but is no longer necessary since 382eb1a23c390154619c385414bdbe6f6e461173 "remove untyped Link<>" removed the old versions. Change-Id: I494025df486a16a45861fcd8192dfe0275b1103c
2016-09-13loplugin:override: No more need for the "MSVC dtor override" workaroundStephan Bergmann
The issue of 362d4f0cd4e50111edfae9d30c90602c37ed65a2 "Explicitly mark overriding destructors as 'virtual'" appears to no longer be a problem with MSVC 2013. (The little change in the rewriting code of compilerplugins/clang/override.cxx was necessary to prevent an endless loop when adding "override" to OOO_DLLPUBLIC_CHARTTOOLS virtual ~CloseableLifeTimeManager(); in chart2/source/inc/LifeTime.hxx, getting stuck in the leading OOO_DLLPUBLIC_CHARTTOOLS macro. Can't remember what that isAtEndOfImmediateMacroExpansion thing was originally necessary for, anyway.) Change-Id: I534c634504d7216b9bb632c2775c04eaf27e927e
2016-04-13loplugin:passstuffbyref in svtoolsNoel Grandin
Change-Id: Ie166eaef65e56fafe4e57a5559b587d7558d7aa4
2016-04-04tdf#97499 Fixed containers parameters clearing #4tymyjan
Change-Id: I7c96181399f4d7e62d4aceca404b22d68f903513 Reviewed-on: https://gerrit.libreoffice.org/23754 Tested-by: Jenkins <ci@libreoffice.org> Reviewed-by: Noel Grandin <noelgrandin@gmail.com>