VirtualBox

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

Last change on this file was 104358, checked in by vboxsync, 5 weeks ago

FE/Qt. bugref:10622. More refactoring around the retranslation functionality.

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

© 2023 Oracle
ContactPrivacy policyTerms of Use