1 /*
2     SPDX-FileCopyrightText: 2003 Richard Moore <rich@kde.org>
3     SPDX-FileCopyrightText: 2003 Ian Reinhart Geiser <geiseri@kde.org>
4     SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau <kossebau@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.0-only
7 */
8 
9 #define TRANSLATION_DOMAIN "kuiviewer"
10 
11 #include "kuiviewer_part.h"
12 
13 // part
14 #include <kuiviewer_part_debug.h>
15 #include "kuiviewer_version.h"
16 // KF
17 #include <KActionCollection>
18 #include <KSelectAction>
19 #include <KConfig>
20 #include <KConfigGroup>
21 #include <KSharedConfig>
22 #include <KLocalizedString>
23 #include <KPluginMetaData>
24 #include <KPluginFactory>
25 
26 // Qt
27 #include <QApplication>
28 #include <QClipboard>
29 #include <QFile>
30 #include <QFormBuilder>
31 #include <QStyle>
32 #include <QStyleFactory>
33 #include <QScrollArea>
34 #include <QMdiArea>
35 #include <QMdiSubWindow>
36 #include <QMimeDatabase>
37 #include <QBuffer>
38 
39 
40 K_PLUGIN_FACTORY_WITH_JSON(KUIViewerPartFactory, "kuiviewer_part.json",
41                            registerPlugin<KUIViewerPart>();)
42 
KUIViewerPart(QWidget * parentWidget,QObject * parent,const KPluginMetaData & metaData,const QVariantList &)43 KUIViewerPart::KUIViewerPart(QWidget* parentWidget,
44                              QObject* parent,
45                              const KPluginMetaData& metaData,
46                              const QVariantList& /*args*/)
47     : KParts::ReadOnlyPart(parent)
48     , m_subWindow(nullptr)
49     , m_view(nullptr)
50 {
51     setMetaData(metaData);
52 
53     // this should be your custom internal widget
54     m_widget = new QMdiArea(parentWidget);
55     m_widget->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
56     m_widget->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
57 
58     // notify the part that this is our internal widget
59     setWidget(m_widget);
60 
61     // set our XML-UI resource file
62     setXMLFile(QStringLiteral("kuiviewer_part.rc"));
63 
64     m_style = actionCollection()->add<KSelectAction>(QStringLiteral("change_style"));
65     m_style->setText(i18n("Style"));
66     connect(m_style, &KSelectAction::indexTriggered,
67             this, &KUIViewerPart::slotStyle);
68     //m_style->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_S));
69     m_style->setEditable(false);
70 
71     m_styleFromConfig = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("currentWidgetStyle", QString());
72 
73     const QStringList styles = QStyleFactory::keys();
74     m_style->setItems(QStringList(i18nc("Default style", "Default")) + styles);
75     m_style->setCurrentItem(0);
76 
77     // empty or incorrect value  means the default value of currentWidgetStyle,
78     // which leads to the Default option.
79     if (!m_styleFromConfig.isEmpty()) {
80         QStringList::ConstIterator it = styles.begin();
81         QStringList::ConstIterator end = styles.end();
82 
83         // Skip the default item
84         int idx = 1;
85         for (; it != end; ++it, ++idx) {
86             if ((*it).toLower() == m_styleFromConfig.toLower()) {
87                 m_style->setCurrentItem(idx);
88                 break;
89             }
90         }
91     }
92 
93     m_style->setToolTip(i18n("Set the style used for the view."));
94     m_style->setMenuAccelsEnabled(true);
95 
96     m_copy = KStandardAction::copy(this, &KUIViewerPart::slotGrab, actionCollection());
97     m_copy->setText(i18n("Copy as Image"));
98 
99     updateActions();
100 
101 // Commented out to fix warning (rich)
102 // slot should probably be called saveAs() for consistency with
103 // KParts::ReadWritePart BTW.
104 //    KStandardAction::saveAs(this, SLOT(slotSave()), actionCollection());
105 }
106 
~KUIViewerPart()107 KUIViewerPart::~KUIViewerPart()
108 {
109 }
110 
designerPluginPaths()111 static QStringList designerPluginPaths()
112 {
113     QStringList paths;
114     const QStringList& libraryPaths = QApplication::libraryPaths();
115     for (const auto& path : libraryPaths) {
116         paths.append(path + QLatin1String("/designer"));
117     }
118     return paths;
119 }
120 
openFile()121 bool KUIViewerPart::openFile()
122 {
123     // m_file is always local so we can use QFile on it
124     QFile file(localFilePath());
125 
126     return loadUiFile(&file);
127 }
128 
doOpenStream(const QString & mimeType)129 bool KUIViewerPart::doOpenStream(const QString& mimeType)
130 {
131     auto mime = QMimeDatabase().mimeTypeForName(mimeType);
132     if (!mime.inherits(QStringLiteral("application/x-designer"))) {
133         return false;
134     }
135 
136     m_streamedData.clear();
137 
138     return true;
139 }
140 
doWriteStream(const QByteArray & data)141 bool KUIViewerPart::doWriteStream(const QByteArray& data)
142 {
143     m_streamedData.append(data);
144     return true;
145 }
146 
doCloseStream()147 bool KUIViewerPart::doCloseStream()
148 {
149     QBuffer buffer(&m_streamedData);
150 
151     const auto success = loadUiFile(&buffer);
152     m_streamedData.clear();
153 
154     return success;
155 }
156 
loadUiFile(QIODevice * device)157 bool KUIViewerPart::loadUiFile(QIODevice* device)
158 {
159     if (!device->open(QIODevice::ReadOnly|QIODevice::Text)) {
160         qCDebug(KUIVIEWERPART) << "Could not open UI file: " << device->errorString();
161         if (m_previousUrl != url()) {
162             // drop previous view state
163             m_previousScrollPosition = QPoint();
164             m_previousSize = QSize();
165         }
166         return false;
167     }
168 
169     if (m_subWindow) {
170         m_widget->removeSubWindow(m_subWindow);
171         delete m_view;
172         delete m_subWindow;
173         m_subWindow = nullptr;
174     }
175 
176     QFormBuilder builder;
177     builder.setPluginPath(designerPluginPaths());
178     m_view = builder.load(device, nullptr);
179 
180     updateActions();
181 
182     if (!m_view) {
183         qCDebug(KUIVIEWERPART) << "Could not load UI file: " << builder.errorString();
184         if (m_previousUrl != url()) {
185             // drop previous view state
186             m_previousScrollPosition = QPoint();
187             m_previousSize = QSize();
188         }
189         return false;
190     }
191 
192     // hack ahead:
193     // UI files have a size set for the widget they define. The QMdiSubWindow relies on sizeHint()
194     // during the show event though it seems, to calculate the initial window size, and then discards
195     // the widget size initially set from the builder in the following layout-ruled geometry update.
196     // Enforcing the initial size by manually setting it afterwards to the widget itself seems not possible,
197     // due to the layout government based on window size.
198     // To inject the initial widget size into the initial window geometry, as hack the min and max sizes are
199     // temporarily set to the wanted size and, once the window is shown, reset to their initial values.
200     const QSize widgetSize = m_view->size();
201     const QSize origWidgetMinimumSize = m_view->minimumSize();
202     const QSize origWidgetMaximumSize = m_view->maximumSize();
203     restyleView(m_style->currentText());
204     m_view->setMinimumSize(widgetSize);
205     m_view->setMaximumSize(widgetSize);
206 
207     const Qt::WindowFlags windowFlags(Qt::SubWindow|Qt::CustomizeWindowHint|Qt::WindowTitleHint);
208     m_subWindow = m_widget->addSubWindow(m_view, windowFlags);
209     // prevent focus stealing by adding the window in disabled state
210     m_subWindow->setEnabled(false);
211     m_subWindow->show();
212     // and restore minimum size
213     m_view->setMinimumSize(origWidgetMinimumSize);
214     m_view->setMaximumSize(origWidgetMaximumSize);
215 
216     m_widget->setActiveSubWindow(m_subWindow);
217     m_subWindow->setEnabled(true);
218 
219     // restore view state if reload
220     if (url() == m_previousUrl) {
221         qCDebug(KUIVIEWERPART) << "Restoring previous view state";
222         m_subWindow->move(m_previousScrollPosition);
223         if (m_previousSize.isValid()) {
224             m_subWindow->resize(m_previousSize);
225         }
226     }
227 
228     return true;
229 }
230 
closeUrl()231 bool KUIViewerPart::closeUrl()
232 {
233     // store view state if file could be loaded
234     // otherwise keep old in case same url will get reloaded again and then successfully
235     if (m_subWindow) {
236         m_previousScrollPosition = m_subWindow->pos();
237         m_previousSize = m_subWindow->size();
238     }
239     // store last used url
240     const auto activeUrl = url();
241     if (activeUrl.isValid()) {
242         m_previousUrl = activeUrl;
243     }
244 
245     m_streamedData.clear();
246 
247     return ReadOnlyPart::closeUrl();
248 }
249 
updateActions()250 void KUIViewerPart::updateActions()
251 {
252     const bool hasView = !m_view.isNull();
253 
254     m_style->setEnabled(hasView);
255     m_copy->setEnabled(hasView);
256 }
257 
restyleView(const QString & styleName)258 void KUIViewerPart::restyleView(const QString& styleName)
259 {
260     QStyle* style = QStyleFactory::create(styleName);
261 
262     m_view->setStyle(style);
263 
264     const QList<QWidget*> childWidgets = m_view->findChildren<QWidget*>();
265     for (auto child : childWidgets) {
266         child->setStyle(style);
267     }
268 }
269 
setWidgetSize(const QSize & size)270 void KUIViewerPart::setWidgetSize(const QSize& size)
271 {
272     if (m_view.isNull()) {
273         return;
274     }
275 
276     // hack: enforce widget size by setting min/max sizes to wanted size
277     // and then have layout update the complete window
278     const QSize origWidgetMinimumSize = m_view->minimumSize();
279     const QSize origWidgetMaximumSize = m_view->maximumSize();
280     m_view->setMinimumSize(size);
281     m_view->setMaximumSize(size);
282     m_subWindow->updateGeometry();
283     // restore
284     m_view->setMinimumSize(origWidgetMinimumSize);
285     m_view->setMaximumSize(origWidgetMaximumSize);
286 }
287 
renderWidgetAsPixmap() const288 QPixmap KUIViewerPart::renderWidgetAsPixmap() const
289 {
290     if (m_view.isNull()) {
291         return QPixmap();
292     }
293 
294     return m_view->grab();
295 }
296 
slotStyle(int)297 void KUIViewerPart::slotStyle(int)
298 {
299     if (m_view.isNull()) {
300         updateActions();
301         return;
302     }
303 
304     m_view->hide();
305 
306     const QString styleName = m_style->currentText();
307     qCDebug(KUIVIEWERPART) << "Style selected:" << styleName;
308     restyleView(styleName);
309 
310     m_view->show();
311 
312     /* the style changed, update the configuration */
313     if (m_styleFromConfig != styleName) {
314         KSharedConfig::Ptr cfg = KSharedConfig::openConfig();
315         KConfigGroup cg(cfg, "General");
316         if (m_style->currentItem() > 0) {
317             /* A style different from the default */
318             cg.writeEntry("currentWidgetStyle", styleName);
319         } else {
320             /* default style: remove the entry */
321             cg.deleteEntry("currentWidgetStyle");
322         }
323         cfg->sync();
324     }
325 }
326 
slotGrab()327 void KUIViewerPart::slotGrab()
328 {
329     if (m_view.isNull()) {
330         updateActions();
331         return;
332     }
333 
334     const QPixmap pixmap = m_view->grab();
335     QApplication::clipboard()->setPixmap(pixmap);
336 }
337 
338 #include "kuiviewer_part.moc"
339