VirtualBox

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

Last change on this file was 104899, checked in by vboxsync, 3 months ago

FE/Qt: UICommon: Get rid of last access media stuff by moving it to UIMediumEnumerator; Adjust the GUI to work accodringly.

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

© 2024 Oracle
ContactPrivacy/Do Not Sell My InfoTerms of Use