VirtualBox

source: vbox/trunk/src/VBox/Frontends/VirtualBox/src/widgets/UIFilePathSelector.cpp@ 100347

Last change on this file since 100347 was 98103, checked in by vboxsync, 21 months ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 20.3 KB
Line 
1/* $Id: UIFilePathSelector.cpp 98103 2023-01-17 14:15:46Z vboxsync $ */
2/** @file
3 * VBox Qt GUI - UIFilePathSelector class implementation.
4 */
5
6/*
7 * Copyright (C) 2008-2023 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28/* Qt includes: */
29#include <QAction>
30#include <QApplication>
31#include <QClipboard>
32#include <QDir>
33#include <QFocusEvent>
34#include <QHBoxLayout>
35#include <QLineEdit>
36#ifdef VBOX_WS_WIN
37# include <QListView>
38#endif
39#include <QRegExp>
40
41/* GUI includes: */
42#include "QIFileDialog.h"
43#include "QILabel.h"
44#include "QILineEdit.h"
45#include "QIToolButton.h"
46#include "UICommon.h"
47#include "UIExtraDataManager.h"
48#include "UIIconPool.h"
49#include "UIFilePathSelector.h"
50
51/* Other VBox includes: */
52#include <iprt/assert.h>
53
54
55/** Returns first position of difference between passed strings. */
56static int differFrom(const QString &str1, const QString &str2)
57{
58 if (str1 == str2)
59 return -1;
60
61 int iMinLength = qMin(str1.size(), str2.size());
62 int iIndex = 0;
63 for (iIndex = 0; iIndex < iMinLength; ++iIndex)
64 if (str1[iIndex] != str2[iIndex])
65 break;
66 return iIndex;
67}
68
69UIFilePathSelector::UIFilePathSelector(QWidget *pParent /* = 0 */)
70 : QIWithRetranslateUI<QIComboBox>(pParent)
71 , m_enmMode(Mode_Folder)
72 , m_strInitialPath(QDir::current().absolutePath())
73 , m_fResetEnabled(true)
74 , m_fEditable(true)
75 , m_fModified(false)
76 , m_fEditableMode(false)
77 , m_fMouseAwaited(false)
78 , m_fToolTipOverriden(false)
79 , m_pCopyAction(new QAction(this))
80 , m_iRecentListSeparatorPosition(ResetId + 1)
81 , m_enmRecentMediaListType(UIMediumDeviceType_Invalid)
82{
83#ifdef VBOX_WS_WIN
84 // WORKAROUND:
85 // On at least Windows host there is a bug with
86 // the QListView which doesn't take into account
87 // the item size change caused by assigning item's
88 // icon of another size or unassigning icon at all.
89 if (view()->inherits("QListView"))
90 qobject_cast<QListView*>(view())->setUniformItemSizes(true);
91#endif /* VBOX_WS_WIN */
92
93 /* Populate items: */
94 insertItem(PathId, "");
95 insertItem(SelectId, "");
96 insertItem(ResetId, "");
97
98 /* Attaching known icons: */
99 setItemIcon(SelectId, UIIconPool::iconSet(":/select_file_16px.png"));
100 setItemIcon(ResetId, UIIconPool::iconSet(":/eraser_16px.png"));
101
102 /* Setup context menu: */
103 addAction(m_pCopyAction);
104 m_pCopyAction->setShortcut(QKeySequence(QKeySequence::Copy));
105 m_pCopyAction->setShortcutContext(Qt::WidgetShortcut);
106
107 /* Initial setup: */
108 setInsertPolicy(QComboBox::NoInsert);
109 setContextMenuPolicy(Qt::ActionsContextMenu);
110 setMinimumWidth(200);
111
112 /* Setup connections: */
113 connect(this, static_cast<void(UIFilePathSelector::*)(int)>(&UIFilePathSelector::activated), this, &UIFilePathSelector::onActivated);
114 connect(m_pCopyAction, &QAction::triggered, this, &UIFilePathSelector::copyToClipboard);
115 connect(&uiCommon(), &UICommon::sigRecentMediaListUpdated, this, &UIFilePathSelector::sltRecentMediaListUpdated);
116
117 /* Editable by default: */
118 setEditable(true);
119
120 /* Applying language settings: */
121 retranslateUi();
122}
123
124void UIFilePathSelector::setMode(Mode enmMode)
125{
126 m_enmMode = enmMode;
127
128 retranslateUi();
129}
130
131void UIFilePathSelector::setEditable(bool fEditable)
132{
133 m_fEditable = fEditable;
134
135 if (m_fEditable)
136 {
137 QIComboBox::setEditable(true);
138
139 /* Install combo-box event-filter: */
140 Assert(comboBox());
141 comboBox()->installEventFilter(this);
142
143 /* Install line-edit connection/event-filter: */
144 Assert(lineEdit());
145 connect(lineEdit(), &QLineEdit::textEdited,
146 this, &UIFilePathSelector::onTextEdited);
147 lineEdit()->installEventFilter(this);
148 }
149 else
150 {
151 if (lineEdit())
152 {
153 /* Remove line-edit event-filter/connection: */
154 lineEdit()->removeEventFilter(this);
155 disconnect(lineEdit(), &QLineEdit::textEdited,
156 this, &UIFilePathSelector::onTextEdited);
157 }
158 if (comboBox())
159 {
160 /* Remove combo-box event-filter: */
161 comboBox()->removeEventFilter(this);
162 }
163 QIComboBox::setEditable(false);
164 }
165}
166
167void UIFilePathSelector::setResetEnabled(bool fEnabled)
168{
169 /* Cache requested state: */
170 m_fResetEnabled = fEnabled;
171
172 /* Update recent list separator position: */
173 m_iRecentListSeparatorPosition = fEnabled ? ResetId + 1 : ResetId;
174
175 if (!fEnabled && count() - 1 == ResetId)
176 removeItem(ResetId);
177 else if (fEnabled && count() - 1 == ResetId - 1)
178 {
179 insertItem(ResetId, "");
180 setItemIcon(ResetId, UIIconPool::iconSet(":/eraser_16px.png"));
181 }
182
183 sltRecentMediaListUpdated(m_enmRecentMediaListType);
184 retranslateUi();
185}
186
187bool UIFilePathSelector::isValid() const
188{
189 if (m_strPath.isNull() || m_strPath.isEmpty())
190 return false;
191 QFileInfo fileInfo(m_strPath);
192 if (!fileInfo.exists() || !fileInfo.isReadable())
193 return false;
194 return true;
195}
196
197void UIFilePathSelector::setToolTip(const QString &strToolTip)
198{
199 /* Call to base-class: */
200 QIComboBox::setToolTip(strToolTip);
201
202 /* Remember if the tool-tip overriden: */
203 m_fToolTipOverriden = !toolTip().isEmpty();
204}
205
206void UIFilePathSelector::setDefaultPath(const QString &strDefaultPath)
207{
208 if (m_strDefaultPath == strDefaultPath)
209 return;
210 m_strDefaultPath = strDefaultPath;
211 if (currentIndex() == ResetId)
212 setPath(m_strDefaultPath);
213}
214
215const QString& UIFilePathSelector::defaultPath() const
216{
217 return m_strDefaultPath;
218}
219
220void UIFilePathSelector::setRecentMediaListType(UIMediumDeviceType enmMediumType)
221{
222 m_enmRecentMediaListType = enmMediumType;
223 sltRecentMediaListUpdated(enmMediumType);
224}
225
226UIMediumDeviceType UIFilePathSelector::recentMediaListType() const
227{
228 return m_enmRecentMediaListType;
229}
230
231void UIFilePathSelector::setPath(const QString &strPath, bool fRefreshText /* = true */)
232{
233 m_strPath = strPath.isEmpty() ? QString() :
234 QDir::toNativeSeparators(strPath);
235 if (fRefreshText)
236 refreshText();
237}
238
239bool UIFilePathSelector::eventFilter(QObject *pObject, QEvent *pEvent)
240{
241 /* If the object is private combo-box: */
242 if (pObject == comboBox())
243 {
244 /* Handle focus events related to private child: */
245 switch (pEvent->type())
246 {
247 case QEvent::FocusIn: focusInEvent(static_cast<QFocusEvent*>(pEvent)); break;
248 case QEvent::FocusOut: focusOutEvent(static_cast<QFocusEvent*>(pEvent)); break;
249 default: break;
250 }
251 }
252
253 /* If the object is private line-edit: */
254 if (pObject == lineEdit())
255 {
256 if (m_fMouseAwaited && (pEvent->type() == QEvent::MouseButtonPress))
257 QMetaObject::invokeMethod(this, "refreshText", Qt::QueuedConnection);
258 }
259
260 /* Call to base-class: */
261 return QIWithRetranslateUI<QIComboBox>::eventFilter(pObject, pEvent);
262}
263
264void UIFilePathSelector::resizeEvent(QResizeEvent *pEvent)
265{
266 QIWithRetranslateUI<QIComboBox>::resizeEvent(pEvent);
267 refreshText();
268}
269
270void UIFilePathSelector::focusInEvent(QFocusEvent *pEvent)
271{
272 if (isPathSelected())
273 {
274 if (m_fEditable)
275 m_fEditableMode = true;
276 if (pEvent->reason() == Qt::MouseFocusReason)
277 m_fMouseAwaited = true;
278 else
279 refreshText();
280 }
281 QIWithRetranslateUI<QIComboBox>::focusInEvent(pEvent);
282}
283
284void UIFilePathSelector::focusOutEvent(QFocusEvent *pEvent)
285{
286 if (isPathSelected())
287 {
288 m_fEditableMode = false;
289 refreshText();
290 }
291 QIWithRetranslateUI<QIComboBox>::focusOutEvent(pEvent);
292}
293
294void UIFilePathSelector::retranslateUi()
295{
296 /* Retranslate copy action: */
297 m_pCopyAction->setText(tr("&Copy"));
298
299 /* Retranslate 'select' item: */
300 setItemText(SelectId, tr("Other..."));
301
302 /* Retranslate 'reset' item: */
303 if (count() - 1 == ResetId)
304 setItemText(ResetId, tr("Reset"));
305
306 /* Set tool-tips of the above two items based on the mode: */
307 switch (m_enmMode)
308 {
309 case Mode_Folder:
310 setItemData(SelectId,
311 tr("Displays a window to select a different folder."),
312 Qt::ToolTipRole);
313 setItemData(ResetId,
314 tr("Resets the folder path to the default value."),
315 Qt::ToolTipRole);
316 break;
317 case Mode_File_Open:
318 case Mode_File_Save:
319 setItemData(SelectId,
320 tr("Displays a window to select a different file."),
321 Qt::ToolTipRole);
322 setItemData(ResetId,
323 tr("Resets the file path to the default value."),
324 Qt::ToolTipRole);
325 break;
326 default:
327 AssertFailedBreak();
328 }
329
330 /* If selector is NOT focused => we interpret the "nothing selected"
331 * item depending on "reset to default" feature state: */
332 if (isResetEnabled())
333 {
334 /* If "reset to default" is enabled: */
335 m_strNoneText = tr("<reset to default>");
336 m_strNoneToolTip = tr("The actual default path value will be displayed after "
337 "accepting the changes and opening this window again.");
338 }
339 else
340 {
341 /* If "reset to default" is NOT enabled: */
342 m_strNoneText = tr("<not selected>");
343 m_strNoneToolTip = tr("Please use the <b>Other...</b> item from the drop-down "
344 "list to select a path.");
345 }
346
347 /* Finally, retranslate current item: */
348 refreshText();
349}
350
351void UIFilePathSelector::onActivated(int iIndex)
352{
353 /* Since the presence of ResetId and position of recent list separator
354 * are dynamical now, we should control condition more carefully: */
355 if (iIndex == SelectId)
356 selectPath();
357 else if (m_fResetEnabled && iIndex == ResetId)
358 {
359 if (m_strDefaultPath.isEmpty())
360 changePath(QString());
361 else
362 changePath(m_strDefaultPath);
363 }
364 else if (iIndex >= m_iRecentListSeparatorPosition)
365 {
366 /* Switch back to Path item early, lineEdit() in refreshText()
367 * should be related to this exactly item: */
368 setCurrentIndex(PathId);
369 changePath(itemText(iIndex));
370 }
371
372 setCurrentIndex(PathId);
373 setFocus();
374}
375
376void UIFilePathSelector::onTextEdited(const QString &strPath)
377{
378 changePath(strPath, false /* refresh text? */);
379}
380
381void UIFilePathSelector::copyToClipboard()
382{
383 QString text(fullPath());
384 /* Copy the current text to the selection and global clipboard. */
385 if (QApplication::clipboard()->supportsSelection())
386 QApplication::clipboard()->setText(text, QClipboard::Selection);
387 QApplication::clipboard()->setText(text, QClipboard::Clipboard);
388}
389
390void UIFilePathSelector::changePath(const QString &strPath,
391 bool fRefreshText /* = true */)
392{
393 const QString strOldPath = QDir::toNativeSeparators(m_strPath);
394 setPath(strPath, fRefreshText);
395 if (!m_fModified && m_strPath != strOldPath)
396 m_fModified = true;
397 emit pathChanged(strPath);
398}
399
400void UIFilePathSelector::selectPath()
401{
402 /* Prepare initial directory: */
403 QString strInitPath;
404 /* If something already chosen: */
405 if (!m_strPath.isEmpty())
406 {
407 /* If that is just a single file/folder (object) name: */
408 const QString strObjectName = QFileInfo(m_strPath).fileName();
409 if (strObjectName == m_strPath)
410 {
411 /* Use the initial path: */
412 strInitPath = m_strInitialPath;
413 }
414 /* If that is full file/folder (object) path: */
415 else
416 {
417 /* Use the first existing dir of m_strPath: */
418 strInitPath = QIFileDialog::getFirstExistingDir(m_strPath);
419 }
420 /* Finally, append object name itself: */
421 strInitPath = QDir(strInitPath).absoluteFilePath(strObjectName);
422 }
423 /* Use the initial path by default: */
424 if (strInitPath.isNull())
425 strInitPath = m_strInitialPath;
426
427 /* Open the choose-file/folder dialog: */
428 QString strSelPath;
429 switch (m_enmMode)
430 {
431 case Mode_File_Open:
432 strSelPath = QIFileDialog::getOpenFileName(strInitPath, m_strFileDialogFilters, window(), m_strFileDialogTitle); break;
433 case Mode_File_Save:
434 {
435 strSelPath = QIFileDialog::getSaveFileName(strInitPath, m_strFileDialogFilters, window(), m_strFileDialogTitle);
436 if (!strSelPath.isEmpty() && QFileInfo(strSelPath).suffix().isEmpty())
437 {
438 if (m_strFileDialogDefaultSaveExtension.isEmpty())
439 strSelPath = QString("%1").arg(strSelPath);
440 else
441 strSelPath = QString("%1.%2").arg(strSelPath).arg(m_strFileDialogDefaultSaveExtension);
442 }
443 break;
444 }
445 case Mode_Folder:
446 strSelPath = QIFileDialog::getExistingDirectory(strInitPath, window(), m_strFileDialogTitle); break;
447 }
448
449 /* Do nothing if nothing chosen: */
450 if (strSelPath.isNull())
451 return;
452
453 /* Wipe out excessive slashes: */
454 strSelPath.remove(QRegularExpression("[\\\\/]$"));
455
456 /* Apply chosen path: */
457 changePath(strSelPath);
458}
459
460QIcon UIFilePathSelector::defaultIcon() const
461{
462 if (m_enmMode == Mode_Folder)
463 return generalIconPool().defaultSystemIcon(QFileIconProvider::Folder);
464 else
465 return generalIconPool().defaultSystemIcon(QFileIconProvider::File);
466}
467
468QString UIFilePathSelector::fullPath(bool fAbsolute /* = true */) const
469{
470 if (m_strPath.isNull())
471 return m_strPath;
472
473 QString strResult;
474 switch (m_enmMode)
475 {
476 case Mode_Folder:
477 strResult = fAbsolute ? QDir(m_strPath).absolutePath() :
478 QDir(m_strPath).path();
479 break;
480 case Mode_File_Open:
481 case Mode_File_Save:
482 strResult = fAbsolute ? QFileInfo(m_strPath).absoluteFilePath() :
483 QFileInfo(m_strPath).filePath();
484 break;
485 default:
486 AssertFailedBreak();
487 }
488 return QDir::toNativeSeparators(strResult);
489}
490
491QString UIFilePathSelector::shrinkText(int iWidth) const
492{
493 QString strFullText(fullPath(false));
494 if (strFullText.isEmpty())
495 return strFullText;
496
497#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
498 int iOldSize = fontMetrics().horizontalAdvance(strFullText);
499 int iIndentSize = fontMetrics().horizontalAdvance("x...x");
500#else
501 int iOldSize = fontMetrics().width(strFullText);
502 int iIndentSize = fontMetrics().width("x...x");
503#endif
504
505 /* Compress text: */
506 int iStart = 0;
507 int iFinish = 0;
508 int iPosition = 0;
509 int iTextWidth = 0;
510 do {
511#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
512 iTextWidth = fontMetrics().horizontalAdvance(strFullText);
513#else
514 iTextWidth = fontMetrics().width(strFullText);
515#endif
516 if (iTextWidth + iIndentSize > iWidth)
517 {
518 iStart = 0;
519 iFinish = strFullText.length();
520
521 /* Selecting remove position: */
522 QRegExp regExp("([\\\\/][^\\\\^/]+[\\\\/]?$)");
523 int iNewFinish = regExp.indexIn(strFullText);
524 if (iNewFinish != -1)
525 iFinish = iNewFinish;
526 iPosition = (iFinish - iStart) / 2;
527
528 if (iPosition == iFinish)
529 break;
530
531 strFullText.remove(iPosition, 1);
532 }
533 } while (iTextWidth + iIndentSize > iWidth);
534
535 strFullText.insert(iPosition, "...");
536#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
537 int newSize = fontMetrics().horizontalAdvance(strFullText);
538#else
539 int newSize = fontMetrics().width(strFullText);
540#endif
541
542 return newSize < iOldSize ? strFullText : fullPath(false);
543}
544
545void UIFilePathSelector::refreshText()
546{
547 if (m_fEditable && m_fEditableMode)
548 {
549 /* Cursor positioning variables: */
550 int iCurPos = -1;
551 int iDiffPos = -1;
552 int iFromRight = -1;
553
554 if (m_fMouseAwaited)
555 {
556 /* Store the cursor position: */
557 iCurPos = lineEdit()->cursorPosition();
558 iDiffPos = differFrom(lineEdit()->text(), m_strPath);
559 iFromRight = lineEdit()->text().size() - iCurPos;
560 }
561
562 /* In editable mode there should be no any icon
563 * and text have be corresponding real stored path
564 * which can be absolute or relative. */
565 if (lineEdit()->text() != m_strPath)
566 setItemText(PathId, m_strPath);
567 setItemIcon(PathId, QIcon());
568
569 /* Set the tool-tip: */
570 if (!m_fToolTipOverriden)
571 QIComboBox::setToolTip(fullPath());
572 setItemData(PathId, toolTip(), Qt::ToolTipRole);
573
574 if (m_fMouseAwaited)
575 {
576 m_fMouseAwaited = false;
577
578 /* Restore the position to the right of dots: */
579 if (iDiffPos != -1 && iCurPos >= iDiffPos + 3)
580 lineEdit()->setCursorPosition(lineEdit()->text().size() -
581 iFromRight);
582 /* Restore the position to the center of text: */
583 else if (iDiffPos != -1 && iCurPos > iDiffPos)
584 lineEdit()->setCursorPosition(lineEdit()->text().size() / 2);
585 /* Restore the position to the left of dots: */
586 else
587 lineEdit()->setCursorPosition(iCurPos);
588 }
589 }
590 else if (m_strPath.isNull())
591 {
592 /* If we are not in editable mode and no path is
593 * stored here - show the translated 'none' string. */
594 if (itemText(PathId) != m_strNoneText)
595 {
596 setItemText(PathId, m_strNoneText);
597 setItemIcon(PathId, QIcon());
598
599 /* Set the tool-tip: */
600 if (!m_fToolTipOverriden)
601 QIComboBox::setToolTip(m_strNoneToolTip);
602 setItemData(PathId, toolTip(), Qt::ToolTipRole);
603 }
604 }
605 else
606 {
607 /* Compress text in combobox: */
608 QStyleOptionComboBox options;
609 options.initFrom(this);
610 QRect rect = QApplication::style()->subControlRect(
611 QStyle::CC_ComboBox, &options, QStyle::SC_ComboBoxEditField);
612 setItemText(PathId, shrinkText(rect.width() - iconSize().width()));
613
614 /* Attach corresponding icon: */
615 setItemIcon(PathId, QFileInfo(m_strPath).exists() ?
616 generalIconPool().defaultFileIcon(QFileInfo(m_strPath)) :
617 defaultIcon());
618
619 /* Set the tool-tip: */
620 if (!m_fToolTipOverriden)
621 QIComboBox::setToolTip(fullPath());
622 setItemData(PathId, toolTip(), Qt::ToolTipRole);
623 }
624}
625
626void UIFilePathSelector::sltRecentMediaListUpdated(UIMediumDeviceType enmMediumType)
627{
628 /* Remove the recent media list from the end of the combo: */
629 while (count() > m_iRecentListSeparatorPosition)
630 removeItem(count() - 1);
631
632 if (enmMediumType != m_enmRecentMediaListType)
633 return;
634 QStringList recentMedia;
635
636 switch (enmMediumType)
637 {
638 case UIMediumDeviceType_DVD:
639 recentMedia = gEDataManager->recentListOfOpticalDisks();
640 break;
641 case UIMediumDeviceType_Floppy:
642 recentMedia = gEDataManager->recentListOfFloppyDisks();
643 break;
644 case UIMediumDeviceType_HardDisk:
645 recentMedia = gEDataManager->recentListOfHardDrives();
646 break;
647 default:
648 break;
649 }
650
651 /* Remove the media which is not there not not readable: */
652 QStringList existingMedia;
653 foreach (QString strMediaPath, recentMedia)
654 {
655 QFileInfo info(strMediaPath);
656 if (info.exists() && info.isReadable())
657 existingMedia << strMediaPath;
658 }
659 if (existingMedia.isEmpty())
660 return;
661
662 insertSeparator(m_iRecentListSeparatorPosition);
663 foreach (const QString strPath, existingMedia)
664 addItem(strPath);
665}
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette