From 30dd5dc4f06e062dc4f351d932d40382638ed654 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 10 Nov 2020 09:03:28 +0100 Subject: [PATCH 001/107] trying to fix CLANG warning --- src/emucore/CartEnhanced.cxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emucore/CartEnhanced.cxx b/src/emucore/CartEnhanced.cxx index 22eb63ee7..487a22741 100644 --- a/src/emucore/CartEnhanced.cxx +++ b/src/emucore/CartEnhanced.cxx @@ -60,7 +60,7 @@ CartridgeEnhanced::CartridgeEnhanced(const ByteBuffer& image, size_t size, void CartridgeEnhanced::install(System& system) { // limit banked RAM size to the size of one RAM bank - const uInt32 ramSize = myRamBankCount > 0 ? 1 << (myBankShift - 1) : uInt32(myRamSize); + const uInt16 ramSize = myRamBankCount > 0 ? 1 << (myBankShift - 1) : uInt16(myRamSize); // calculate bank switching and RAM sizes and masks myBankSize = 1 << myBankShift; // e.g. = 2 ^ 12 = 4K = 0x1000 @@ -93,7 +93,7 @@ void CartridgeEnhanced::install(System& system) // Set the page accessing method for the RAM writing pages // Note: Writes are mapped to poke() (NOT using direcPokeBase) to check for read from write port (RWP) access.type = System::PageAccessType::WRITE; - for(size_t addr = ROM_OFFSET + myWriteOffset; addr < ROM_OFFSET + myWriteOffset + myRamSize; addr += System::PAGE_SIZE) + for(uInt16 addr = ROM_OFFSET + myWriteOffset; addr < ROM_OFFSET + myWriteOffset + myRamSize; addr += System::PAGE_SIZE) { const uInt16 offset = addr & myRamMask; @@ -105,7 +105,7 @@ void CartridgeEnhanced::install(System& system) // Set the page accessing method for the RAM reading pages access.type = System::PageAccessType::READ; - for(size_t addr = ROM_OFFSET + myReadOffset; addr < ROM_OFFSET + myReadOffset + myRamSize; addr += System::PAGE_SIZE) + for(uInt16 addr = ROM_OFFSET + myReadOffset; addr < ROM_OFFSET + myReadOffset + myRamSize; addr += System::PAGE_SIZE) { const uInt16 offset = addr & myRamMask; From d3b9f52b089eb9abf78cb8e340850be668daef57 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 10 Nov 2020 19:53:36 +0100 Subject: [PATCH 002/107] initial commit for #719 --- src/emucore/FrameBuffer.cxx | 2 +- src/gui/ContextMenu.cxx | 2 +- src/gui/Dialog.cxx | 93 +++++++++++++++++------- src/gui/Dialog.hxx | 7 +- src/gui/DialogContainer.cxx | 42 ++++++++--- src/gui/GuiObject.hxx | 5 ++ src/gui/ScrollBarWidget.cxx | 1 + src/gui/TabWidget.cxx | 60 +++++++++------- src/gui/Widget.cxx | 139 ++++++++++++++++++++++-------------- src/gui/Widget.hxx | 2 + 10 files changed, 235 insertions(+), 118 deletions(-) diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index 97bbb2153..e922ff390 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -439,7 +439,7 @@ void FrameBuffer::update(bool force) force = force || myOSystem.launcher().needsRedraw(); if(force) { - clear(); + //clear(); myOSystem.launcher().draw(force); } break; // EventHandlerState::LAUNCHER diff --git a/src/gui/ContextMenu.cxx b/src/gui/ContextMenu.cxx index 41f049ba2..9cf626657 100644 --- a/src/gui/ContextMenu.cxx +++ b/src/gui/ContextMenu.cxx @@ -621,5 +621,5 @@ void ContextMenu::drawDialog() s.drawBitmap(_downImg, ((_w-_x)>>1)-4, (_rowHeight>>1)+y-4, _scrollDnColor, _arrowSize); } - setDirty(); + clearDirty(); } diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 85c95349f..6e81535c2 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -149,6 +149,35 @@ void Dialog::center() positionAt(instance().settings().getInt("dialogpos")); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Dialog::setDirty() +{ + _dirty = true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool Dialog::isDirty() const +{ + return _dirty; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool Dialog::isChainDirty() const +{ + bool dirty = false; + + // Check if widget or any subwidgets are dirty + Widget* w = _firstWidget; + + while(!dirty && w) + { + dirty |= w->needsRedraw(); + w = w->_next; + } + + return dirty; +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Dialog::positionAt(uInt32 pos) { @@ -192,7 +221,9 @@ void Dialog::positionAt(uInt32 pos) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool Dialog::render() { - if(!_dirty || !isVisible()) + //assert(_dirty); + + if(!isVisible() || !needsRedraw()) return false; // Draw this dialog @@ -207,7 +238,7 @@ bool Dialog::render() surface->render(); }); } - _dirty = false; + //_dirty = false; return true; } @@ -371,37 +402,49 @@ void Dialog::drawDialog() FBSurface& s = surface(); - // Dialog is still on top if e.g a ContextMenu is opened - _onTop = parent().myDialogStack.top() == this - || (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this - && !parent().myDialogStack.top()->hasTitle()); - - if(_flags & Widget::FLAG_CLEARBG) + if(isDirty()) { - // cerr << "Dialog::drawDialog(): w = " << _w << ", h = " << _h << " @ " << &s << endl << endl; - s.fillRect(_x, _y + _th, _w, _h - _th, _onTop ? kDlgColor : kBGColorLo); - if(_th) - { - s.fillRect(_x, _y, _w, _th, _onTop ? kColorTitleBar : kColorTitleBarLo); - s.drawString(_font, _title, _x + _font.getMaxCharWidth() * 1.25, _y + _font.getFontHeight() / 6, - _font.getStringWidth(_title), - _onTop ? kColorTitleText : kColorTitleTextLo); - } - } - else - s.invalidate(); - if(_flags & Widget::FLAG_BORDER) // currently only used by Dialog itself - s.frameRect(_x, _y, _w, _h, _onTop ? kColor : kShadowColor); + //cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; + + // Dialog is still on top if e.g a ContextMenu is opened + _onTop = parent().myDialogStack.top() == this + || (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this + && !parent().myDialogStack.top()->hasTitle()); + + if(_flags & Widget::FLAG_CLEARBG) + { + // cerr << "Dialog::drawDialog(): w = " << _w << ", h = " << _h << " @ " << &s << endl << endl; + s.fillRect(_x, _y + _th, _w, _h - _th, _onTop ? kDlgColor : kBGColorLo); + if(_th) + { + s.fillRect(_x, _y, _w, _th, _onTop ? kColorTitleBar : kColorTitleBarLo); + s.drawString(_font, _title, _x + _font.getMaxCharWidth() * 1.25, _y + _font.getFontHeight() / 6, + _font.getStringWidth(_title), + _onTop ? kColorTitleText : kColorTitleTextLo); + } + } + else { + s.invalidate(); + cerr << "invalidate " << typeid(*this).name() << endl; + } + if(_flags & Widget::FLAG_BORDER) // currently only used by Dialog itself + s.frameRect(_x, _y, _w, _h, _onTop ? kColor : kShadowColor); + + // Make all child widgets dirty + Widget::setDirtyInChain(_firstWidget); + + clearDirty(); + } - // Make all child widget dirty Widget* w = _firstWidget; - Widget::setDirtyInChain(w); // Draw all children w = _firstWidget; while(w) { - w->draw(); + // only redraw changed widgets + if(w->needsRedraw()) + w->draw(); w = w->_next; } diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index 1f6bbec60..805522c42 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -64,8 +64,9 @@ class Dialog : public GuiObject // A dialog being dirty indicates that its underlying surface needs to be // redrawn and then re-rendered; this is taken care of in ::render() - void setDirty() override { _dirty = true; } - bool isDirty() const { return _dirty; } + void setDirty() override; + bool isDirty() const override; + bool isChainDirty() const override; bool render(); void addFocusWidget(Widget* w) override; @@ -235,7 +236,7 @@ class Dialog : public GuiObject int _tabID{0}; int _flags{0}; - bool _dirty{false}; + //bool _dirty{false}; uInt32 _max_w{0}; // maximum wanted width uInt32 _max_h{0}; // maximum wanted height diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 950d71c06..af353f452 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -91,21 +91,32 @@ void DialogContainer::updateTime(uInt64 time) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool DialogContainer::draw(bool full) { + cerr << "draw " << full << endl; if(myDialogStack.empty()) return false; // Make the top dialog dirty if a full redraw is requested - if(full) - myDialogStack.top()->setDirty(); + //if(full) + // myDialogStack.top()->setDirty(); // If the top dialog is dirty, then all below it must be redrawn too const bool dirty = needsRedraw(); + //if(dirty) + // myDialogStack.top()->setDirty(); - myDialogStack.applyAll([&](Dialog*& d){ - if(dirty) - d->setDirty(); - full |= d->render(); - }); + //myDialogStack.applyAll([&](Dialog*& d){ + // if(dirty) + // d->setDirty(); + // full |= d->render(); + //}); + //if(dirty) + { + myDialogStack.applyAll([&](Dialog*& d) { + if(d->needsRedraw()) + //d->setDirty(); + full |= d->render(); + }); + } return full; } @@ -113,7 +124,9 @@ bool DialogContainer::draw(bool full) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool DialogContainer::needsRedraw() const { - return !myDialogStack.empty() ? myDialogStack.top()->isDirty() : false; + return !myDialogStack.empty() + ? myDialogStack.top()->needsRedraw() + : false; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -133,6 +146,9 @@ int DialogContainer::addDialog(Dialog* d) "Unable to show dialog box; FIX THE CODE"); else { + // fade out current top dialog + if(!myDialogStack.empty()) + myDialogStack.top()->setDirty(); d->setDirty(); myDialogStack.push(d); } @@ -145,8 +161,16 @@ void DialogContainer::removeDialog() if(!myDialogStack.empty()) { myDialogStack.pop(); + // necessary as long as all dialogs share the same surface if(!myDialogStack.empty()) - myDialogStack.top()->setDirty(); + { + //myDialogStack.top()->setDirty(); + + // Mark all dialogs for redraw + myDialogStack.applyAll([&](Dialog*& d){ + d->setDirty(); + }); + } } } diff --git a/src/gui/GuiObject.hxx b/src/gui/GuiObject.hxx index 77392209c..2f4a5c1ca 100644 --- a/src/gui/GuiObject.hxx +++ b/src/gui/GuiObject.hxx @@ -78,6 +78,10 @@ class GuiObject : public CommandReceiver virtual bool isVisible() const = 0; virtual void setDirty() = 0; + virtual void clearDirty() { _dirty = false; } + virtual bool isDirty() const { return _dirty; } + virtual bool isChainDirty() const = 0; + virtual bool needsRedraw() const { return isDirty() || isChainDirty(); }; /** Add given widget(s) to the focus list */ virtual void addFocusWidget(Widget* w) = 0; @@ -104,6 +108,7 @@ class GuiObject : public CommandReceiver protected: int _x{0}, _y{0}, _w{0}, _h{0}; + bool _dirty{false}; Widget* _firstWidget{nullptr}; WidgetArray _focusList; diff --git a/src/gui/ScrollBarWidget.cxx b/src/gui/ScrollBarWidget.cxx index 335b716de..451f4a3ee 100644 --- a/src/gui/ScrollBarWidget.cxx +++ b/src/gui/ScrollBarWidget.cxx @@ -315,6 +315,7 @@ void ScrollBarWidget::drawWidget(bool hilite) s.fillRect(_x + 1, _y + _sliderPos - 1, _w - 2, _sliderHeight + 2, onTop ? (hilite && _part == Part::Slider) ? kScrollColorHi : kScrollColor : kColor); } + clearDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/TabWidget.cxx b/src/gui/TabWidget.cxx index f1b92b876..61f42b350 100644 --- a/src/gui/TabWidget.cxx +++ b/src/gui/TabWidget.cxx @@ -275,39 +275,45 @@ void TabWidget::drawWidget(bool hilite) // The tab widget is strange in that it acts as both a widget (obviously) // and a dialog (it contains other widgets). Because of the latter, // it must assume responsibility for refreshing all its children. - Widget::setDirtyInChain(_tabs[_activeTab].firstWidget); - FBSurface& s = dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); - - // Iterate over all tabs and draw them - int i, x = _x + kTabLeftOffset; - for (i = 0; i < int(_tabs.size()); ++i) + if(isDirty()) { - int tabWidth = _tabs[i].tabWidth ? _tabs[i].tabWidth : _tabWidth; - ColorId fontcolor = _tabs[i].enabled && onTop? kTextColor : kColor; - int yOffset = (i == _activeTab) ? 0 : 1; - s.fillRect(x, _y + 1, tabWidth, _tabHeight - 1, - (i == _activeTab) - ? onTop ? kDlgColor : kBGColorLo - : onTop ? kBGColorHi : kDlgColor); // ? kWidColor : kDlgColor - s.drawString(_font, _tabs[i].title, x + kTabPadding + yOffset, - _y + yOffset + (_tabHeight - _lineHeight - 1), - tabWidth - 2 * kTabPadding, fontcolor, TextAlign::Center); - if(i == _activeTab) + FBSurface& s = dialog().surface(); + bool onTop = _boss->dialog().isOnTop(); + + // Iterate over all tabs and draw them + int i, x = _x + kTabLeftOffset; + for(i = 0; i < int(_tabs.size()); ++i) { - s.hLine(x, _y, x + tabWidth - 1, onTop ? kWidColor : kDlgColor); - s.vLine(x + tabWidth, _y + 1, _y + _tabHeight - 1, onTop ? kBGColorLo : kColor); + int tabWidth = _tabs[i].tabWidth ? _tabs[i].tabWidth : _tabWidth; + ColorId fontcolor = _tabs[i].enabled && onTop ? kTextColor : kColor; + int yOffset = (i == _activeTab) ? 0 : 1; + s.fillRect(x, _y + 1, tabWidth, _tabHeight - 1, + (i == _activeTab) + ? onTop ? kDlgColor : kBGColorLo + : onTop ? kBGColorHi : kDlgColor); // ? kWidColor : kDlgColor + s.drawString(_font, _tabs[i].title, x + kTabPadding + yOffset, + _y + yOffset + (_tabHeight - _lineHeight - 1), + tabWidth - 2 * kTabPadding, fontcolor, TextAlign::Center); + if(i == _activeTab) + { + s.hLine(x, _y, x + tabWidth - 1, onTop ? kWidColor : kDlgColor); + s.vLine(x + tabWidth, _y + 1, _y + _tabHeight - 1, onTop ? kBGColorLo : kColor); + } + else + s.hLine(x, _y + _tabHeight, x + tabWidth, onTop ? kWidColor : kDlgColor); + + x += tabWidth + kTabSpacing; } - else - s.hLine(x, _y + _tabHeight, x + tabWidth, onTop ? kWidColor : kDlgColor); - x += tabWidth + kTabSpacing; + // fill empty right space + s.hLine(x - kTabSpacing + 1, _y + _tabHeight, _x + _w - 1, onTop ? kWidColor : kDlgColor); + s.hLine(_x, _y + _h - 1, _x + _w - 1, onTop ? kBGColorLo : kColor); + + clearDirty(); + // Make all child widgets of currently active tab dirty + Widget::setDirtyInChain(_tabs[_activeTab].firstWidget); } - - // fill empty right space - s.hLine(x - kTabSpacing + 1, _y + _tabHeight, _x + _w - 1, onTop ? kWidColor : kDlgColor); - s.hLine(_x, _y + _h - 1, _x + _w - 1, onTop ? kBGColorLo : kColor); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index a54d9e45c..33f6b632d 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -56,7 +56,41 @@ void Widget::setDirty() { // A widget being dirty indicates that its parent dialog is dirty // So we inform the parent about it - _boss->dialog().setDirty(); + //_boss->dialog().setDirty(); + //cerr << "set dirty " << typeid(*this).name() << endl; + + _dirty = true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool Widget::isDirty() const +{ + string name = typeid(*this).name(); + if(_dirty && name == "class TabWidget") + cerr << "is dirty " << typeid(*this).name() << endl; + + return _dirty; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool Widget::isChainDirty() const +{ + string name = typeid(*this).name(); + if(_dirty && name == "class TabWidget") + cerr << "is chain dirty " << typeid(*this).name() << endl; + + bool dirty = false; + + // Check if widget or any subwidgets are dirty + Widget* w = _firstWidget; + + while(!dirty && w) + { + dirty |= w->isDirty(); + w = w->_next; + } + + return dirty; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -65,60 +99,67 @@ void Widget::draw() if(!isVisible() || !_boss->isVisible()) return; - FBSurface& s = _boss->dialog().surface(); - - bool onTop = _boss->dialog().isOnTop(); - - bool hasBorder = _flags & Widget::FLAG_BORDER; // currently only used by Dialog widget - int oldX = _x, oldY = _y; - - // Account for our relative position in the dialog - _x = getAbsX(); - _y = getAbsY(); - - // Clear background (unless alpha blending is enabled) - if(_flags & Widget::FLAG_CLEARBG) + if(isDirty()) { - int x = _x, y = _y, w = _w, h = _h; + //cerr << " *** draw widget " << typeid(*this).name() << " ***" << endl; + + FBSurface& s = _boss->dialog().surface(); + + bool onTop = _boss->dialog().isOnTop(); + + bool hasBorder = _flags & Widget::FLAG_BORDER; // currently only used by Dialog widget + int oldX = _x, oldY = _y; + + // Account for our relative position in the dialog + _x = getAbsX(); + _y = getAbsY(); + + // Clear background (unless alpha blending is enabled) + if(_flags & Widget::FLAG_CLEARBG) + { + int x = _x, y = _y, w = _w, h = _h; + if(hasBorder) + { + x++; y++; w -= 2; h -= 2; + } + s.fillRect(x, y, w, h, !onTop ? _bgcolorlo : (_flags & Widget::FLAG_HILITED) && isEnabled() ? _bgcolorhi : _bgcolor); + } + + // Draw border if(hasBorder) { - x++; y++; w-=2; h-=2; + s.frameRect(_x, _y, _w, _h, !onTop ? kColor : (_flags & Widget::FLAG_HILITED) && isEnabled() ? kWidColorHi : kColor); + _x += 4; + _y += 4; + _w -= 8; + _h -= 8; } - s.fillRect(x, y, w, h, !onTop ? _bgcolorlo : (_flags & Widget::FLAG_HILITED) && isEnabled() ? _bgcolorhi : _bgcolor); + + // Now perform the actual widget draw + drawWidget((_flags & Widget::FLAG_HILITED) ? true : false); + + // Restore x/y + if(hasBorder) + { + _x -= 4; + _y -= 4; + _w += 8; + _h += 8; + } + + _x = oldX; + _y = oldY; } - // Draw border - if(hasBorder) - { - s.frameRect(_x, _y, _w, _h, !onTop ? kColor : (_flags & Widget::FLAG_HILITED) && isEnabled() ? kWidColorHi : kColor); - _x += 4; - _y += 4; - _w -= 8; - _h -= 8; - } - - // Now perform the actual widget draw - drawWidget((_flags & Widget::FLAG_HILITED) ? true : false); - - // Restore x/y - if (hasBorder) - { - _x -= 4; - _y -= 4; - _w += 8; - _h += 8; - } - - _x = oldX; - _y = oldY; - // Draw all children Widget* w = _firstWidget; while(w) { - w->draw(); + if(w->needsRedraw()) + w->draw(); w = w->_next; } + clearDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -290,6 +331,7 @@ void Widget::setDirtyInChain(Widget* start) { while(start) { + //cerr << "setDirtyInChain " << typeid(*start).name() << endl; start->setDirty(); start = start->_next; } @@ -345,8 +387,6 @@ void StaticTextWidget::drawWidget(bool hilite) bool onTop = _boss->dialog().isOnTop(); s.drawString(_font, _label, _x, _y, _w, isEnabled() && onTop ? _textcolor : kColor, _align, 0, true, _shadowcolor); - - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -454,6 +494,7 @@ void ButtonWidget::setBitmap(const uInt32* bitmap, int bmw, int bmh) _bmh = bmh; _bmw = bmw; + cerr << "setBitmap" << endl; setDirty(); } @@ -474,8 +515,6 @@ void ButtonWidget::drawWidget(bool hilite) !(isEnabled() && onTop) ? _textcolorlo : hilite ? _textcolorhi : _textcolor, _bmw, _bmh); - - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -635,8 +674,6 @@ void CheckboxWidget::drawWidget(bool hilite) // Finally draw the label s.drawString(_font, _label, _x + prefixSize(_font), _y + _textY, _w, onTop && isEnabled() ? kTextColor : kColor); - - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -652,7 +689,7 @@ SliderWidget::SliderWidget(GuiObject* boss, const GUI::Font& font, _valueLabelWidth(valueLabelWidth), _forceLabelSign(forceLabelSign) { - _flags = Widget::FLAG_ENABLED | Widget::FLAG_TRACK_MOUSE; + _flags = Widget::FLAG_ENABLED | Widget::FLAG_TRACK_MOUSE | Widget::FLAG_CLEARBG;; _bgcolor = kDlgColor; _bgcolorhi = kDlgColor; @@ -870,8 +907,6 @@ void SliderWidget::drawWidget(bool hilite) if(_valueLabelWidth > 0) s.drawString(_font, _valueLabel + _valueUnit, _x + _w - _valueLabelWidth, _y + 2, _valueLabelWidth, isEnabled() ? kTextColor : kColor); - - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 50e71681e..c5fd8dced 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -83,6 +83,8 @@ class Widget : public GuiObject virtual bool handleEvent(Event::Type event) { return false; } void setDirty() override; + bool isDirty() const override; + bool isChainDirty() const override; void draw() override; void receivedFocus(); void lostFocus(); From de5233d63b3bd8d24e5f9b96d4291036cb024ae4 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 10 Nov 2020 23:29:56 +0100 Subject: [PATCH 003/107] added support of transparent widgets (for TimeMachineDialog) --- src/common/FBSurfaceSDL2.cxx | 16 ++++++++++++++++ src/common/FBSurfaceSDL2.hxx | 2 ++ src/emucore/FBSurface.hxx | 11 +++++++++++ src/gui/Dialog.cxx | 8 ++++++-- src/gui/DialogContainer.cxx | 2 +- src/gui/TimeLineWidget.cxx | 18 +++++++++++------- src/gui/TimeMachineDialog.cxx | 3 +++ src/gui/Widget.cxx | 13 ++++++++----- src/gui/Widget.hxx | 4 +++- 9 files changed, 61 insertions(+), 16 deletions(-) diff --git a/src/common/FBSurfaceSDL2.cxx b/src/common/FBSurfaceSDL2.cxx index 543195165..398c773ba 100644 --- a/src/common/FBSurfaceSDL2.cxx +++ b/src/common/FBSurfaceSDL2.cxx @@ -176,6 +176,22 @@ void FBSurfaceSDL2::invalidate() SDL_FillRect(mySurface, nullptr, 0); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void FBSurfaceSDL2::invalidateRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h) +{ + ASSERT_MAIN_THREAD; + + // Clear the rectangle + SDL_Rect tmp; + tmp.x = x; + tmp.y = y; + tmp.w = w; + tmp.h = h; + // Note: Transparency has to be 0 to clear the rectangle foreground + // without affecting the background display. + SDL_FillRect(mySurface, &tmp, 0); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FBSurfaceSDL2::free() { diff --git a/src/common/FBSurfaceSDL2.hxx b/src/common/FBSurfaceSDL2.hxx index f446bb17b..c19808f27 100644 --- a/src/common/FBSurfaceSDL2.hxx +++ b/src/common/FBSurfaceSDL2.hxx @@ -55,6 +55,8 @@ class FBSurfaceSDL2 : public FBSurface void translateCoords(Int32& x, Int32& y) const override; bool render() override; void invalidate() override; + void invalidateRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h) override; + void free() override; void reload() override; void resize(uInt32 width, uInt32 height) override; diff --git a/src/emucore/FBSurface.hxx b/src/emucore/FBSurface.hxx index a018ac2c9..67eaa86d2 100644 --- a/src/emucore/FBSurface.hxx +++ b/src/emucore/FBSurface.hxx @@ -325,6 +325,17 @@ class FBSurface */ virtual void invalidate() = 0; + /** + This method should be called to reset a surface area to empty + + @param x The x coordinate + @param y The y coordinate + @param w The width of the area + @param h The height of the area + */ + virtual void invalidateRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h) = 0; + + /** This method should be called to free any resources being used by the surface. diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 6e81535c2..c691c545a 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -404,7 +404,7 @@ void Dialog::drawDialog() if(isDirty()) { - //cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; + cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; // Dialog is still on top if e.g a ContextMenu is opened _onTop = parent().myDialogStack.top() == this @@ -414,7 +414,11 @@ void Dialog::drawDialog() if(_flags & Widget::FLAG_CLEARBG) { // cerr << "Dialog::drawDialog(): w = " << _w << ", h = " << _h << " @ " << &s << endl << endl; - s.fillRect(_x, _y + _th, _w, _h - _th, _onTop ? kDlgColor : kBGColorLo); + + if(_flags & Widget::FLAG_TRANSPARENT) + s.invalidateRect(_x, _y + _th, _w, _h - _th); + else + s.fillRect(_x, _y + _th, _w, _h - _th, _onTop ? kDlgColor : kBGColorLo); if(_th) { s.fillRect(_x, _y, _w, _th, _onTop ? kColorTitleBar : kColorTitleBarLo); diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index af353f452..8577eb50f 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -91,7 +91,7 @@ void DialogContainer::updateTime(uInt64 time) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool DialogContainer::draw(bool full) { - cerr << "draw " << full << endl; + //cerr << "draw " << full << endl; if(myDialogStack.empty()) return false; diff --git a/src/gui/TimeLineWidget.cxx b/src/gui/TimeLineWidget.cxx index 053d7bccf..94c4d3ef9 100644 --- a/src/gui/TimeLineWidget.cxx +++ b/src/gui/TimeLineWidget.cxx @@ -35,8 +35,11 @@ TimeLineWidget::TimeLineWidget(GuiObject* boss, const GUI::Font& font, : ButtonWidget(boss, font, x, y, w, h, label, cmd), _labelWidth(labelWidth) { - _flags = Widget::FLAG_ENABLED | Widget::FLAG_TRACK_MOUSE; + _flags = Widget::FLAG_ENABLED | Widget::FLAG_TRACK_MOUSE + | Widget::FLAG_CLEARBG | Widget::FLAG_TRANSPARENT; + _bgcolor = kDlgColor; + //_bgcolor = kBGColor; _bgcolorhi = kDlgColor; if(!_label.empty() && _labelWidth == 0) @@ -84,7 +87,7 @@ void TimeLineWidget::setStepValues(const IntArray& steps) if(steps.size() > _stepValue.capacity()) _stepValue.reserve(2 * steps.size()); - double scale = (_w - _labelWidth - 2 - HANDLE_W*0) / double(steps.back()); + double scale = (_w - _labelWidth - 2 - HANDLE_W) / double(steps.back()); // Skip the very last value; we take care of it outside the end of the loop for(uInt32 i = 0; i < steps.size() - 1; ++i) @@ -92,7 +95,7 @@ void TimeLineWidget::setStepValues(const IntArray& steps) // Due to integer <-> double conversion, the last value is sometimes // slightly less than the maximum value; we assign it manually to fix this - _stepValue.push_back(_w - _labelWidth - 2 - HANDLE_W*0); + _stepValue.push_back(_w - _labelWidth - 2 - HANDLE_W); } else _stepValue.push_back(0); @@ -141,17 +144,18 @@ void TimeLineWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); + cerr << "TimeLineWidget::drawWidget " << typeid(s).name() << endl; + // Draw the label, if any if(_labelWidth > 0) s.drawString(_font, _label, _x, _y + 2, _labelWidth, isEnabled() ? kTextColor : kColor, TextAlign::Left); - int p = valueToPos(_value), - x = _x + _labelWidth, - w = _w - _labelWidth; - // Frame the handle const int HANDLE_W2 = (HANDLE_W + 1) / 2; + int p = valueToPos(_value), + x = _x + _labelWidth + HANDLE_W2, + w = _w - _labelWidth - HANDLE_W; s.hLine(x + p - HANDLE_W2, _y + 0, x + p - HANDLE_W2 + HANDLE_W, kColorInfo); s.vLine(x + p - HANDLE_W2, _y + 1, _y + _h - 2, kColorInfo); s.hLine(x + p - HANDLE_W2 + 1, _y + _h - 1, x + p - HANDLE_W2 + 1 + HANDLE_W, kBGColor); diff --git a/src/gui/TimeMachineDialog.cxx b/src/gui/TimeMachineDialog.cxx index 3b591a62d..5982ba693 100644 --- a/src/gui/TimeMachineDialog.cxx +++ b/src/gui/TimeMachineDialog.cxx @@ -225,6 +225,7 @@ TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent, // Add index info myCurrentIdxWidget = new StaticTextWidget(this, font, xpos, ypos, "1000", TextAlign::Left, kBGColor); myCurrentIdxWidget->setTextColor(kColorInfo); + myCurrentIdxWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_TRANSPARENT); myLastIdxWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("1000"), ypos, "1000", TextAlign::Right, kBGColor); myLastIdxWidget->setTextColor(kColorInfo); @@ -241,6 +242,7 @@ TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent, // Add time info int ypos_s = ypos + (buttonHeight - font.getFontHeight() + 1) / 2; // align to button vertical center myCurrentTimeWidget = new StaticTextWidget(this, font, xpos, ypos_s, "00:00.00", TextAlign::Left, kBGColor); + myCurrentTimeWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_TRANSPARENT); myCurrentTimeWidget->setTextColor(kColorInfo); myLastTimeWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("00:00.00"), ypos_s, "00:00.00", TextAlign::Right, kBGColor); @@ -287,6 +289,7 @@ TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent, // Add message myMessageWidget = new StaticTextWidget(this, font, xpos, ypos_s, " ", TextAlign::Left, kBGColor); + myMessageWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_TRANSPARENT); myMessageWidget->setTextColor(kColorInfo); } diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 33f6b632d..3c0252688 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -65,9 +65,9 @@ void Widget::setDirty() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool Widget::isDirty() const { - string name = typeid(*this).name(); - if(_dirty && name == "class TabWidget") - cerr << "is dirty " << typeid(*this).name() << endl; + //string name = typeid(*this).name(); + //if(_dirty && name == "class TabWidget") + // cerr << "is dirty " << typeid(*this).name() << endl; return _dirty; } @@ -101,7 +101,7 @@ void Widget::draw() if(isDirty()) { - //cerr << " *** draw widget " << typeid(*this).name() << " ***" << endl; + cerr << " *** draw widget " << typeid(*this).name() << " ***" << endl; FBSurface& s = _boss->dialog().surface(); @@ -122,7 +122,10 @@ void Widget::draw() { x++; y++; w -= 2; h -= 2; } - s.fillRect(x, y, w, h, !onTop ? _bgcolorlo : (_flags & Widget::FLAG_HILITED) && isEnabled() ? _bgcolorhi : _bgcolor); + if(isTransparent()) + s.invalidateRect(x, y, w, h); + else + s.fillRect(x, y, w, h, !onTop ? _bgcolorlo : (_flags & Widget::FLAG_HILITED) && isEnabled() ? _bgcolorhi : _bgcolor); } // Draw border diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index c5fd8dced..15e7a1d9c 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -52,7 +52,8 @@ class Widget : public GuiObject FLAG_TRACK_MOUSE = 1 << 5, FLAG_RETAIN_FOCUS = 1 << 6, FLAG_WANTS_TAB = 1 << 7, - FLAG_WANTS_RAWDATA = 1 << 8 + FLAG_WANTS_RAWDATA = 1 << 8, + FLAG_TRANSPARENT = 1 << 9 }; public: @@ -105,6 +106,7 @@ class Widget : public GuiObject virtual bool wantsFocus() const { return _flags & FLAG_RETAIN_FOCUS; } bool wantsTab() const { return _flags & FLAG_WANTS_TAB; } bool wantsRaw() const { return _flags & FLAG_WANTS_RAWDATA; } + bool isTransparent() const { return _flags & FLAG_TRANSPARENT; } void setID(uInt32 id) { _id = id; } uInt32 getID() const { return _id; } From 49fcb524fd1cb1e3794ecdf1d1f9ada6a0a3ae83 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 11 Nov 2020 08:56:11 +0100 Subject: [PATCH 004/107] move Widget flags into GuiObject --- src/emucore/FBSurface.hxx | 2 +- src/gui/Dialog.cxx | 14 +++++++------- src/gui/Dialog.hxx | 6 ------ src/gui/GuiObject.hxx | 29 ++++++++++++++++++++++++++--- src/gui/TimeLineWidget.cxx | 3 +-- src/gui/TimeMachineDialog.cxx | 9 ++++++--- src/gui/Widget.cxx | 27 ++++++++++++++++----------- src/gui/Widget.hxx | 20 -------------------- src/libretro/FBSurfaceLIBRETRO.hxx | 1 + 9 files changed, 58 insertions(+), 53 deletions(-) diff --git a/src/emucore/FBSurface.hxx b/src/emucore/FBSurface.hxx index 67eaa86d2..d72ffdd64 100644 --- a/src/emucore/FBSurface.hxx +++ b/src/emucore/FBSurface.hxx @@ -323,7 +323,7 @@ class FBSurface This method should be called to reset the surface to empty pixels / colour black. */ - virtual void invalidate() = 0; + virtual void invalidate() {}; /** This method should be called to reset a surface area to empty diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index c691c545a..f519cdadc 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -49,9 +49,9 @@ Dialog::Dialog(OSystem& instance, DialogContainer& parent, const GUI::Font& font const string& title, int x, int y, int w, int h) : GuiObject(instance, parent, *this, x, y, w, h), _font(font), - _title(title), - _flags(Widget::FLAG_ENABLED | Widget::FLAG_BORDER | Widget::FLAG_CLEARBG) + _title(title) { + _flags = Widget::FLAG_ENABLED | Widget::FLAG_BORDER | Widget::FLAG_CLEARBG; setTitle(title); } @@ -411,14 +411,14 @@ void Dialog::drawDialog() || (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this && !parent().myDialogStack.top()->hasTitle()); - if(_flags & Widget::FLAG_CLEARBG) + if(clearsBackground()) { // cerr << "Dialog::drawDialog(): w = " << _w << ", h = " << _h << " @ " << &s << endl << endl; - if(_flags & Widget::FLAG_TRANSPARENT) - s.invalidateRect(_x, _y + _th, _w, _h - _th); - else + if(hasBackground()) s.fillRect(_x, _y + _th, _w, _h - _th, _onTop ? kDlgColor : kBGColorLo); + else + s.invalidateRect(_x, _y + _th, _w, _h - _th); if(_th) { s.fillRect(_x, _y, _w, _th, _onTop ? kColorTitleBar : kColorTitleBarLo); @@ -431,7 +431,7 @@ void Dialog::drawDialog() s.invalidate(); cerr << "invalidate " << typeid(*this).name() << endl; } - if(_flags & Widget::FLAG_BORDER) // currently only used by Dialog itself + if(hasBorder()) // currently only used by Dialog itself s.frameRect(_x, _y, _w, _h, _onTop ? kColor : kShadowColor); // Make all child widgets dirty diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index 805522c42..d9b750ab6 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -90,10 +90,6 @@ class Dialog : public GuiObject */ void addSurface(const shared_ptr& surface); - void setFlags(int flags) { _flags |= flags; setDirty(); } - void clearFlags(int flags) { _flags &= ~flags; setDirty(); } - int getFlags() const { return _flags; } - void setTitle(const string& title); bool hasTitle() { return !_title.empty(); } @@ -235,8 +231,6 @@ class Dialog : public GuiObject shared_ptr _surface; int _tabID{0}; - int _flags{0}; - //bool _dirty{false}; uInt32 _max_w{0}; // maximum wanted width uInt32 _max_h{0}; // maximum wanted height diff --git a/src/gui/GuiObject.hxx b/src/gui/GuiObject.hxx index 2f4a5c1ca..3bb510a6b 100644 --- a/src/gui/GuiObject.hxx +++ b/src/gui/GuiObject.hxx @@ -41,6 +41,20 @@ class GuiObject : public CommandReceiver friend class Widget; friend class DialogContainer; + public: + enum : uInt32 { + FLAG_ENABLED = 1 << 0, + FLAG_INVISIBLE = 1 << 1, + FLAG_HILITED = 1 << 2, + FLAG_BORDER = 1 << 3, + FLAG_CLEARBG = 1 << 4, + FLAG_TRACK_MOUSE = 1 << 5, + FLAG_RETAIN_FOCUS = 1 << 6, + FLAG_WANTS_TAB = 1 << 7, + FLAG_WANTS_RAWDATA = 1 << 8, + FLAG_NOBG = 1 << 9 + }; + public: // The commands generated by various widgets enum { @@ -83,6 +97,14 @@ class GuiObject : public CommandReceiver virtual bool isChainDirty() const = 0; virtual bool needsRedraw() const { return isDirty() || isChainDirty(); }; + void setFlags(uInt32 flags) { _flags |= flags; setDirty(); } + void clearFlags(uInt32 flags) { _flags &= ~flags; setDirty(); } + uInt32 getFlags() const { return _flags; } + + bool hasBorder() const { return _flags & FLAG_BORDER; } + bool clearsBackground() const { return _flags & FLAG_CLEARBG; } + bool hasBackground() const { return !(_flags & FLAG_NOBG); } + /** Add given widget(s) to the focus list */ virtual void addFocusWidget(Widget* w) = 0; virtual void addToFocusList(WidgetArray& list) = 0; @@ -107,10 +129,11 @@ class GuiObject : public CommandReceiver Dialog& myDialog; protected: - int _x{0}, _y{0}, _w{0}, _h{0}; - bool _dirty{false}; + int _x{0}, _y{0}, _w{0}, _h{0}; + bool _dirty{false}; + uInt32 _flags{0}; - Widget* _firstWidget{nullptr}; + Widget* _firstWidget{nullptr}; WidgetArray _focusList; private: diff --git a/src/gui/TimeLineWidget.cxx b/src/gui/TimeLineWidget.cxx index 94c4d3ef9..4e6b9e9a0 100644 --- a/src/gui/TimeLineWidget.cxx +++ b/src/gui/TimeLineWidget.cxx @@ -36,10 +36,9 @@ TimeLineWidget::TimeLineWidget(GuiObject* boss, const GUI::Font& font, _labelWidth(labelWidth) { _flags = Widget::FLAG_ENABLED | Widget::FLAG_TRACK_MOUSE - | Widget::FLAG_CLEARBG | Widget::FLAG_TRANSPARENT; + | Widget::FLAG_CLEARBG | Widget::FLAG_NOBG; _bgcolor = kDlgColor; - //_bgcolor = kBGColor; _bgcolorhi = kDlgColor; if(!_label.empty() && _labelWidth == 0) diff --git a/src/gui/TimeMachineDialog.cxx b/src/gui/TimeMachineDialog.cxx index 5982ba693..e822504a9 100644 --- a/src/gui/TimeMachineDialog.cxx +++ b/src/gui/TimeMachineDialog.cxx @@ -218,6 +218,7 @@ TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent, this->clearFlags(Widget::FLAG_CLEARBG); // does only work combined with blending (0..100)! this->clearFlags(Widget::FLAG_BORDER); + this->setFlags(Widget::FLAG_NOBG); xpos = H_BORDER; ypos = V_BORDER; @@ -225,9 +226,10 @@ TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent, // Add index info myCurrentIdxWidget = new StaticTextWidget(this, font, xpos, ypos, "1000", TextAlign::Left, kBGColor); myCurrentIdxWidget->setTextColor(kColorInfo); - myCurrentIdxWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_TRANSPARENT); + myCurrentIdxWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG); myLastIdxWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("1000"), ypos, "1000", TextAlign::Right, kBGColor); + myLastIdxWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG); myLastIdxWidget->setTextColor(kColorInfo); // Add timeline @@ -242,10 +244,11 @@ TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent, // Add time info int ypos_s = ypos + (buttonHeight - font.getFontHeight() + 1) / 2; // align to button vertical center myCurrentTimeWidget = new StaticTextWidget(this, font, xpos, ypos_s, "00:00.00", TextAlign::Left, kBGColor); - myCurrentTimeWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_TRANSPARENT); + myCurrentTimeWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG); myCurrentTimeWidget->setTextColor(kColorInfo); myLastTimeWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("00:00.00"), ypos_s, "00:00.00", TextAlign::Right, kBGColor); + myLastTimeWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG); myLastTimeWidget->setTextColor(kColorInfo); xpos = myCurrentTimeWidget->getRight() + BUTTON_GAP * 4; @@ -289,7 +292,7 @@ TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent, // Add message myMessageWidget = new StaticTextWidget(this, font, xpos, ypos_s, " ", TextAlign::Left, kBGColor); - myMessageWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_TRANSPARENT); + myMessageWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG); myMessageWidget->setTextColor(kColorInfo); } diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 3c0252688..6358808e7 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -106,8 +106,6 @@ void Widget::draw() FBSurface& s = _boss->dialog().surface(); bool onTop = _boss->dialog().isOnTop(); - - bool hasBorder = _flags & Widget::FLAG_BORDER; // currently only used by Dialog widget int oldX = _x, oldY = _y; // Account for our relative position in the dialog @@ -115,23 +113,29 @@ void Widget::draw() _y = getAbsY(); // Clear background (unless alpha blending is enabled) - if(_flags & Widget::FLAG_CLEARBG) + if(clearsBackground()) { int x = _x, y = _y, w = _w, h = _h; - if(hasBorder) + if(hasBorder()) { x++; y++; w -= 2; h -= 2; } - if(isTransparent()) - s.invalidateRect(x, y, w, h); + if(hasBackground()) + s.fillRect(x, y, w, h, !onTop + ? _bgcolorlo + : (_flags & Widget::FLAG_HILITED) && isEnabled() + ? _bgcolorhi : _bgcolor); else - s.fillRect(x, y, w, h, !onTop ? _bgcolorlo : (_flags & Widget::FLAG_HILITED) && isEnabled() ? _bgcolorhi : _bgcolor); + s.invalidateRect(x, y, w, h); } // Draw border - if(hasBorder) + if(hasBorder()) { - s.frameRect(_x, _y, _w, _h, !onTop ? kColor : (_flags & Widget::FLAG_HILITED) && isEnabled() ? kWidColorHi : kColor); + s.frameRect(_x, _y, _w, _h, !onTop + ? kColor + : (_flags & Widget::FLAG_HILITED) && isEnabled() + ? kWidColorHi : kColor); _x += 4; _y += 4; _w -= 8; @@ -142,7 +146,7 @@ void Widget::draw() drawWidget((_flags & Widget::FLAG_HILITED) ? true : false); // Restore x/y - if(hasBorder) + if(hasBorder()) { _x -= 4; _y -= 4; @@ -350,6 +354,7 @@ StaticTextWidget::StaticTextWidget(GuiObject* boss, const GUI::Font& font, _align(align) { _flags = Widget::FLAG_ENABLED; + _bgcolor = kDlgColor; _bgcolorhi = kDlgColor; _textcolor = kTextColor; @@ -692,7 +697,7 @@ SliderWidget::SliderWidget(GuiObject* boss, const GUI::Font& font, _valueLabelWidth(valueLabelWidth), _forceLabelSign(forceLabelSign) { - _flags = Widget::FLAG_ENABLED | Widget::FLAG_TRACK_MOUSE | Widget::FLAG_CLEARBG;; + _flags = Widget::FLAG_ENABLED | Widget::FLAG_TRACK_MOUSE | Widget::FLAG_CLEARBG; _bgcolor = kDlgColor; _bgcolorhi = kDlgColor; diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 15e7a1d9c..270f1eb32 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -42,20 +42,6 @@ class Widget : public GuiObject { friend class Dialog; - public: - enum : uInt32 { - FLAG_ENABLED = 1 << 0, - FLAG_INVISIBLE = 1 << 1, - FLAG_HILITED = 1 << 2, - FLAG_BORDER = 1 << 3, - FLAG_CLEARBG = 1 << 4, - FLAG_TRACK_MOUSE = 1 << 5, - FLAG_RETAIN_FOCUS = 1 << 6, - FLAG_WANTS_TAB = 1 << 7, - FLAG_WANTS_RAWDATA = 1 << 8, - FLAG_TRANSPARENT = 1 << 9 - }; - public: Widget(GuiObject* boss, const GUI::Font& font, int x, int y, int w, int h); ~Widget() override; @@ -97,16 +83,11 @@ class Widget : public GuiObject /** Set/clear FLAG_ENABLED */ void setEnabled(bool e); - void setFlags(uInt32 flags) { _flags |= flags; setDirty(); } - void clearFlags(uInt32 flags) { _flags &= ~flags; setDirty(); } - uInt32 getFlags() const { return _flags; } - bool isEnabled() const { return _flags & FLAG_ENABLED; } bool isVisible() const override { return !(_flags & FLAG_INVISIBLE); } virtual bool wantsFocus() const { return _flags & FLAG_RETAIN_FOCUS; } bool wantsTab() const { return _flags & FLAG_WANTS_TAB; } bool wantsRaw() const { return _flags & FLAG_WANTS_RAWDATA; } - bool isTransparent() const { return _flags & FLAG_TRANSPARENT; } void setID(uInt32 id) { _id = id; } uInt32 getID() const { return _id; } @@ -140,7 +121,6 @@ class Widget : public GuiObject const GUI::Font& _font; Widget* _next{nullptr}; uInt32 _id{0}; - uInt32 _flags{0}; bool _hasFocus{false}; int _fontWidth{0}; int _lineHeight{0}; diff --git a/src/libretro/FBSurfaceLIBRETRO.hxx b/src/libretro/FBSurfaceLIBRETRO.hxx index 646d2c46c..3918ff673 100644 --- a/src/libretro/FBSurfaceLIBRETRO.hxx +++ b/src/libretro/FBSurfaceLIBRETRO.hxx @@ -51,6 +51,7 @@ class FBSurfaceLIBRETRO : public FBSurface void translateCoords(Int32& x, Int32& y) const override { } bool render() override { return true; } void invalidate() override { } + void invalidateRect(uInt32, uInt32, uInt32, uInt32) override { } void free() override { } void reload() override { } void resize(uInt32 width, uInt32 height) override { } From 7a1a5e9c17d71f191ec6b37cc9daf30aad6cb673 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 11 Nov 2020 13:00:44 +0100 Subject: [PATCH 005/107] added individual size to each save state (fixes #727) --- src/common/RewindManager.cxx | 24 ++++++++++++++---------- src/common/RewindManager.hxx | 2 -- src/emucore/Serializer.cxx | 2 ++ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/common/RewindManager.cxx b/src/common/RewindManager.cxx index 70fa481d7..bc6b70813 100644 --- a/src/common/RewindManager.cxx +++ b/src/common/RewindManager.cxx @@ -37,7 +37,6 @@ RewindManager::RewindManager(OSystem& system, StateManager& statemgr) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void RewindManager::setup() { - myStateSize = 0; myLastTimeMachineAdd = false; const string& prefix = myOSystem.settings().getBool("dev.settings") ? "dev." : "plr."; @@ -138,7 +137,6 @@ bool RewindManager::addState(const string& message, bool timeMachine) s.rewind(); // rewind Serializer internal buffers if(myStateManager.saveState(s) && myOSystem.console().tia().saveDisplay(s)) { - myStateSize = std::max(myStateSize, uInt32(s.size())); state.message = message; state.cycles = myOSystem.console().tia().cycles(); myLastTimeMachineAdd = timeMachine; @@ -256,18 +254,22 @@ string RewindManager::saveAllStates() buf.str(""); out.putString(STATE_HEADER); out.putShort(numStates); - out.putInt(myStateSize); - unique_ptr buffer = make_unique(myStateSize); for (uInt32 i = 0; i < numStates; ++i) { RewindState& state = myStateList.current(); Serializer& s = state.data; + uInt32 stateSize = uInt32(s.size()); + unique_ptr buffer = make_unique(stateSize); + + out.putInt(stateSize); + // Rewind Serializer internal buffers s.rewind(); + // Save state - s.getByteArray(buffer.get(), myStateSize); - out.putByteArray(buffer.get(), myStateSize); + s.getByteArray(buffer.get(), stateSize); + out.putByteArray(buffer.get(), stateSize); out.putString(state.message); out.putLong(state.cycles); @@ -310,25 +312,27 @@ string RewindManager::loadAllStates() if (in.getString() != STATE_HEADER) return "Incompatible all states file"; numStates = in.getShort(); - myStateSize = in.getInt(); - unique_ptr buffer = make_unique(myStateSize); for (uInt32 i = 0; i < numStates; ++i) { if (myStateList.full()) compressStates(); + uInt32 stateSize = in.getInt(); + unique_ptr buffer = make_unique(stateSize); + // Add new state at the end of the list (queue adds at end) // This updates the 'current' iterator inside the list myStateList.addLast(); RewindState& state = myStateList.current(); Serializer& s = state.data; + // Rewind Serializer internal buffers s.rewind(); // Fill new state with saved values - in.getByteArray(buffer.get(), myStateSize); - s.putByteArray(buffer.get(), myStateSize); + in.getByteArray(buffer.get(), stateSize); + s.putByteArray(buffer.get(), stateSize); state.message = in.getString(); state.cycles = in.getLong(); } diff --git a/src/common/RewindManager.hxx b/src/common/RewindManager.hxx index 619af7264..5f53083f7 100644 --- a/src/common/RewindManager.hxx +++ b/src/common/RewindManager.hxx @@ -144,7 +144,6 @@ class RewindManager bool atLast() const { return myStateList.atLast(); } void resize(uInt32 size) { myStateList.resize(size); } void clear() { - myStateSize = 0; myStateList.clear(); } @@ -176,7 +175,6 @@ class RewindManager uInt64 myHorizon{0}; double myFactor{0.0}; bool myLastTimeMachineAdd{false}; - uInt32 myStateSize{0}; struct RewindState { Serializer data; // actual save state diff --git a/src/emucore/Serializer.cxx b/src/emucore/Serializer.cxx index 80282bdc2..7f94dced2 100644 --- a/src/emucore/Serializer.cxx +++ b/src/emucore/Serializer.cxx @@ -91,6 +91,8 @@ void Serializer::rewind() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - size_t Serializer::size() const { + myStream->seekp(0, std::ios::end); + return myStream->tellp(); } From 515ef088df8b11670fad53c8d7b9cc22f11ba1c0 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 11 Nov 2020 13:00:44 +0100 Subject: [PATCH 006/107] added individual size to each save state (fixes #727) --- src/common/RewindManager.cxx | 24 ++++++++++++++---------- src/common/RewindManager.hxx | 2 -- src/emucore/Serializer.cxx | 2 ++ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/common/RewindManager.cxx b/src/common/RewindManager.cxx index 70fa481d7..bc6b70813 100644 --- a/src/common/RewindManager.cxx +++ b/src/common/RewindManager.cxx @@ -37,7 +37,6 @@ RewindManager::RewindManager(OSystem& system, StateManager& statemgr) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void RewindManager::setup() { - myStateSize = 0; myLastTimeMachineAdd = false; const string& prefix = myOSystem.settings().getBool("dev.settings") ? "dev." : "plr."; @@ -138,7 +137,6 @@ bool RewindManager::addState(const string& message, bool timeMachine) s.rewind(); // rewind Serializer internal buffers if(myStateManager.saveState(s) && myOSystem.console().tia().saveDisplay(s)) { - myStateSize = std::max(myStateSize, uInt32(s.size())); state.message = message; state.cycles = myOSystem.console().tia().cycles(); myLastTimeMachineAdd = timeMachine; @@ -256,18 +254,22 @@ string RewindManager::saveAllStates() buf.str(""); out.putString(STATE_HEADER); out.putShort(numStates); - out.putInt(myStateSize); - unique_ptr buffer = make_unique(myStateSize); for (uInt32 i = 0; i < numStates; ++i) { RewindState& state = myStateList.current(); Serializer& s = state.data; + uInt32 stateSize = uInt32(s.size()); + unique_ptr buffer = make_unique(stateSize); + + out.putInt(stateSize); + // Rewind Serializer internal buffers s.rewind(); + // Save state - s.getByteArray(buffer.get(), myStateSize); - out.putByteArray(buffer.get(), myStateSize); + s.getByteArray(buffer.get(), stateSize); + out.putByteArray(buffer.get(), stateSize); out.putString(state.message); out.putLong(state.cycles); @@ -310,25 +312,27 @@ string RewindManager::loadAllStates() if (in.getString() != STATE_HEADER) return "Incompatible all states file"; numStates = in.getShort(); - myStateSize = in.getInt(); - unique_ptr buffer = make_unique(myStateSize); for (uInt32 i = 0; i < numStates; ++i) { if (myStateList.full()) compressStates(); + uInt32 stateSize = in.getInt(); + unique_ptr buffer = make_unique(stateSize); + // Add new state at the end of the list (queue adds at end) // This updates the 'current' iterator inside the list myStateList.addLast(); RewindState& state = myStateList.current(); Serializer& s = state.data; + // Rewind Serializer internal buffers s.rewind(); // Fill new state with saved values - in.getByteArray(buffer.get(), myStateSize); - s.putByteArray(buffer.get(), myStateSize); + in.getByteArray(buffer.get(), stateSize); + s.putByteArray(buffer.get(), stateSize); state.message = in.getString(); state.cycles = in.getLong(); } diff --git a/src/common/RewindManager.hxx b/src/common/RewindManager.hxx index 619af7264..5f53083f7 100644 --- a/src/common/RewindManager.hxx +++ b/src/common/RewindManager.hxx @@ -144,7 +144,6 @@ class RewindManager bool atLast() const { return myStateList.atLast(); } void resize(uInt32 size) { myStateList.resize(size); } void clear() { - myStateSize = 0; myStateList.clear(); } @@ -176,7 +175,6 @@ class RewindManager uInt64 myHorizon{0}; double myFactor{0.0}; bool myLastTimeMachineAdd{false}; - uInt32 myStateSize{0}; struct RewindState { Serializer data; // actual save state diff --git a/src/emucore/Serializer.cxx b/src/emucore/Serializer.cxx index 80282bdc2..7f94dced2 100644 --- a/src/emucore/Serializer.cxx +++ b/src/emucore/Serializer.cxx @@ -91,6 +91,8 @@ void Serializer::rewind() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - size_t Serializer::size() const { + myStream->seekp(0, std::ios::end); + return myStream->tellp(); } From 113ee123986166e099efb046b5f3410c8a8723d4 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 11 Nov 2020 15:37:32 +0100 Subject: [PATCH 007/107] activated enhanced "full" redraw logic --- src/emucore/FrameBuffer.cxx | 32 ++++++++++++++++---------------- src/gui/DialogContainer.cxx | 29 ++++++++--------------------- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index e922ff390..828c14e8f 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -323,13 +323,14 @@ void FrameBuffer::update(bool force) // last, since they are always drawn on top of everything else). // Full rendering is required when messages are enabled - force = force || myMsg.counter >= 0; + force |= (myMsg.counter >= 0); // Detect when a message has been turned off; one last redraw is required // in this case, to draw over the area that the message occupied if(myMsg.counter == 0) myMsg.counter = -1; + bool redraw = false; switch(myOSystem.eventHandler().state()) { case EventHandlerState::NONE: @@ -354,8 +355,8 @@ void FrameBuffer::update(bool force) #ifdef GUI_SUPPORT case EventHandlerState::OPTIONSMENU: { - force = force || myOSystem.menu().needsRedraw(); - if(force) + redraw = myOSystem.menu().needsRedraw(); + if(force || redraw) { clear(); myTIASurface->render(); @@ -366,8 +367,8 @@ void FrameBuffer::update(bool force) case EventHandlerState::CMDMENU: { - force = force || myOSystem.commandMenu().needsRedraw(); - if(force) + redraw = myOSystem.commandMenu().needsRedraw(); + if(force || redraw) { clear(); myTIASurface->render(); @@ -378,8 +379,8 @@ void FrameBuffer::update(bool force) case EventHandlerState::MESSAGEMENU: { - force = force || myOSystem.messageMenu().needsRedraw(); - if (force) + redraw = myOSystem.messageMenu().needsRedraw(); + if(force || redraw) { clear(); myTIASurface->render(); @@ -390,8 +391,8 @@ void FrameBuffer::update(bool force) case EventHandlerState::TIMEMACHINE: { - force = force || myOSystem.timeMachine().needsRedraw(); - if(force) + redraw = myOSystem.timeMachine().needsRedraw(); + if(force || redraw) { clear(); myTIASurface->render(); @@ -436,10 +437,9 @@ void FrameBuffer::update(bool force) case EventHandlerState::LAUNCHER: { - force = force || myOSystem.launcher().needsRedraw(); - if(force) + redraw = myOSystem.launcher().needsRedraw(); + if(force || redraw) { - //clear(); myOSystem.launcher().draw(force); } break; // EventHandlerState::LAUNCHER @@ -449,10 +449,10 @@ void FrameBuffer::update(bool force) #ifdef DEBUGGER_SUPPORT case EventHandlerState::DEBUGGER: { - force = force || myOSystem.debugger().needsRedraw(); - if(force) + redraw = myOSystem.debugger().needsRedraw(); + if(force || redraw) { - clear(); + myOSystem.debugger().draw(force); } break; // EventHandlerState::DEBUGGER @@ -471,7 +471,7 @@ void FrameBuffer::update(bool force) drawMessage(); // Push buffers to screen only when necessary - if(force) + if(force || redraw) myBackend->renderToScreen(); } diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 8577eb50f..60bc9b32d 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -91,32 +91,19 @@ void DialogContainer::updateTime(uInt64 time) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool DialogContainer::draw(bool full) { - //cerr << "draw " << full << endl; + cerr << "draw " << full << " " << typeid(*this).name() << endl; if(myDialogStack.empty()) return false; // Make the top dialog dirty if a full redraw is requested - //if(full) - // myDialogStack.top()->setDirty(); + if(full) + myDialogStack.top()->setDirty(); - // If the top dialog is dirty, then all below it must be redrawn too - const bool dirty = needsRedraw(); - //if(dirty) - // myDialogStack.top()->setDirty(); - - //myDialogStack.applyAll([&](Dialog*& d){ - // if(dirty) - // d->setDirty(); - // full |= d->render(); - //}); - //if(dirty) - { - myDialogStack.applyAll([&](Dialog*& d) { - if(d->needsRedraw()) - //d->setDirty(); - full |= d->render(); - }); - } + // Render all dirty dialogs + myDialogStack.applyAll([&](Dialog*& d) { + if(d->needsRedraw()) + full |= d->render(); + }); return full; } From 250a1634de40c312cb835c3e55d15fa1b5aee6de Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 11 Nov 2020 16:50:49 +0100 Subject: [PATCH 008/107] fixed RomInfoWidget drawing --- src/gui/RomInfoWidget.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gui/RomInfoWidget.cxx b/src/gui/RomInfoWidget.cxx index 01b30015b..c2068a4d1 100644 --- a/src/gui/RomInfoWidget.cxx +++ b/src/gui/RomInfoWidget.cxx @@ -172,6 +172,8 @@ void RomInfoWidget::parseProperties(const FilesystemNode& node) myRomInfo.push_back("Controllers: " + (left + " (left), " + right + " (right)")); if (bsDetected != "") myRomInfo.push_back("Type: " + Bankswitch::typeToDesc(Bankswitch::nameToType(bsDetected))); + + setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -227,4 +229,5 @@ void RomInfoWidget::drawWidget(bool hilite) onTop ? _textcolor : _shadowcolor); ypos += _font.getLineHeight() + (lines - 1) * _font.getFontHeight(); } + clearDirty(); } From 678892e8c7fdbe36a5c133dd2a70e42c9eceb24b Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 11 Nov 2020 17:26:40 +0100 Subject: [PATCH 009/107] added blinking cursor --- src/gui/Dialog.cxx | 7 +++-- src/gui/Dialog.hxx | 2 +- src/gui/EditableWidget.cxx | 55 +++++++++++++++++++++++++++++++------- src/gui/EditableWidget.hxx | 4 +++ src/gui/GuiObject.hxx | 4 +-- src/gui/Widget.cxx | 4 +-- src/gui/Widget.hxx | 2 +- 7 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index f519cdadc..669d7341c 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -156,7 +156,7 @@ void Dialog::setDirty() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool Dialog::isDirty() const +bool Dialog::isDirty() { return _dirty; } @@ -221,8 +221,6 @@ void Dialog::positionAt(uInt32 pos) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool Dialog::render() { - //assert(_dirty); - if(!isVisible() || !needsRedraw()) return false; @@ -404,7 +402,7 @@ void Dialog::drawDialog() if(isDirty()) { - cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; + //cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; // Dialog is still on top if e.g a ContextMenu is opened _onTop = parent().myDialogStack.top() == this @@ -446,6 +444,7 @@ void Dialog::drawDialog() w = _firstWidget; while(w) { + // only redraw changed widgets if(w->needsRedraw()) w->draw(); diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index d9b750ab6..68a71608c 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -65,7 +65,7 @@ class Dialog : public GuiObject // A dialog being dirty indicates that its underlying surface needs to be // redrawn and then re-rendered; this is taken care of in ::render() void setDirty() override; - bool isDirty() const override; + bool isDirty() override; // TODO: remove bool isChainDirty() const override; bool render(); diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index 16b371813..b1783c10c 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -62,6 +62,23 @@ void EditableWidget::setText(const string& str, bool) setDirty(); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool EditableWidget::isDirty() +{ + if(_hasFocus && _editable && isVisible() && _boss->isVisible()) + { + _caretTimer++; + if(_caretTimer > 40) // switch every 2/3rd seconds + { + _caretTimer = 0; + _caretEnabled = !_caretEnabled; + _dirty = true; + } + cerr << "."; + } + + return _dirty; +} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void EditableWidget::setEditable(bool editable, bool hiliteBG) @@ -79,6 +96,15 @@ void EditableWidget::setEditable(bool editable, bool hiliteBG) } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EditableWidget::receivedFocusWidget() +{ + _caretTimer = 0; + _caretEnabled = true; + + Widget::receivedFocusWidget(); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void EditableWidget::lostFocusWidget() { @@ -316,22 +342,31 @@ void EditableWidget::drawCaretSelection() if (!_editable || !isVisible() || !_boss->isVisible() || !_hasFocus) return; - const Common::Rect& editRect = getEditRect(); - int x = editRect.x(); - int y = editRect.y(); + if(_caretEnabled) + { + FBSurface& s = _boss->dialog().surface(); + const Common::Rect& editRect = getEditRect(); + int x = editRect.x(); + int y = editRect.y(); + x += getCaretOffset(); - x += getCaretOffset(); + x += _x; + y += _y; - x += _x; - y += _y; - - FBSurface& s = _boss->dialog().surface(); - s.vLine(x, y + 2, y + editRect.h() - 2, kTextColorHi); - s.vLine(x-1, y + 2, y + editRect.h() - 2, kTextColorHi); + s.vLine(x, y + 2, y + editRect.h() - 2, kTextColorHi); + s.vLine(x-1, y + 2, y + editRect.h() - 2, kTextColorHi); + clearDirty(); + } if(_selectSize) { + FBSurface& s = _boss->dialog().surface(); + const Common::Rect& editRect = getEditRect(); + int x = editRect.x(); + int y = editRect.y(); + string text = selectString(); + x = editRect.x(); y = editRect.y(); int w = editRect.w(); diff --git a/src/gui/EditableWidget.hxx b/src/gui/EditableWidget.hxx index fda92ccdf..e4dfbc2f7 100644 --- a/src/gui/EditableWidget.hxx +++ b/src/gui/EditableWidget.hxx @@ -65,7 +65,9 @@ class EditableWidget : public Widget, public CommandSender void setTextFilter(const TextFilter& filter) { _filter = filter; } protected: + void receivedFocusWidget() override; void lostFocusWidget() override; + bool isDirty() override; virtual void startEditMode() { setFlags(Widget::FLAG_WANTS_RAWDATA); } virtual void endEditMode() { clearFlags(Widget::FLAG_WANTS_RAWDATA); } @@ -110,6 +112,8 @@ class EditableWidget : public Widget, public CommandSender unique_ptr myUndoHandler; int _caretPos{0}; + int _caretTimer{0}; + bool _caretEnabled{true}; // Size of current selected text // 0 = no selection diff --git a/src/gui/GuiObject.hxx b/src/gui/GuiObject.hxx index 3bb510a6b..a6cf4e5ca 100644 --- a/src/gui/GuiObject.hxx +++ b/src/gui/GuiObject.hxx @@ -93,9 +93,9 @@ class GuiObject : public CommandReceiver virtual bool isVisible() const = 0; virtual void setDirty() = 0; virtual void clearDirty() { _dirty = false; } - virtual bool isDirty() const { return _dirty; } + virtual bool isDirty() { return _dirty; } virtual bool isChainDirty() const = 0; - virtual bool needsRedraw() const { return isDirty() || isChainDirty(); }; + virtual bool needsRedraw() { return isDirty() || isChainDirty(); }; void setFlags(uInt32 flags) { _flags |= flags; setDirty(); } void clearFlags(uInt32 flags) { _flags &= ~flags; setDirty(); } diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 6358808e7..c9e0441e7 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -63,7 +63,7 @@ void Widget::setDirty() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool Widget::isDirty() const +bool Widget::isDirty() { //string name = typeid(*this).name(); //if(_dirty && name == "class TabWidget") @@ -353,7 +353,7 @@ StaticTextWidget::StaticTextWidget(GuiObject* boss, const GUI::Font& font, _label(text), _align(align) { - _flags = Widget::FLAG_ENABLED; + _flags = Widget::FLAG_ENABLED | FLAG_CLEARBG; _bgcolor = kDlgColor; _bgcolorhi = kDlgColor; diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 270f1eb32..3e70523c0 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -70,7 +70,7 @@ class Widget : public GuiObject virtual bool handleEvent(Event::Type event) { return false; } void setDirty() override; - bool isDirty() const override; + bool isDirty() override; // TODO: remove bool isChainDirty() const override; void draw() override; void receivedFocus(); From bec842b9d72379e80ec7d5f45d8442362df6e16b Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 11 Nov 2020 18:24:30 +0100 Subject: [PATCH 010/107] removed some superfluous redraws --- src/debugger/gui/DataGridWidget.cxx | 8 ++++---- src/debugger/gui/RomListWidget.cxx | 8 ++++---- src/debugger/gui/TiaZoomWidget.cxx | 8 ++++---- src/debugger/gui/ToggleWidget.cxx | 8 ++++---- src/gui/Dialog.cxx | 29 ++++++++++++++--------------- src/gui/EditTextWidget.cxx | 8 ++++---- src/gui/GuiObject.hxx | 18 ++++++++++++++++-- src/gui/PopUpWidget.cxx | 8 ++++---- src/gui/ScrollBarWidget.cxx | 8 ++++---- src/gui/StringListWidget.cxx | 8 ++++---- src/gui/TabWidget.cxx | 8 ++++---- src/gui/Widget.cxx | 14 +++++++++----- 12 files changed, 75 insertions(+), 58 deletions(-) diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index f3b81676c..32358dd05 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -242,15 +242,15 @@ void DataGridWidget::setRange(int lower, int upper) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DataGridWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DataGridWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 48885f734..20fc53052 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -285,15 +285,15 @@ void RomListWidget::handleMouseWheel(int x, int y, int direction) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void RomListWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void RomListWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/debugger/gui/TiaZoomWidget.cxx b/src/debugger/gui/TiaZoomWidget.cxx index e4553f361..a008c4444 100644 --- a/src/debugger/gui/TiaZoomWidget.cxx +++ b/src/debugger/gui/TiaZoomWidget.cxx @@ -181,15 +181,15 @@ void TiaZoomWidget::handleMouseMoved(int x, int y) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TiaZoomWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TiaZoomWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED); myMouseMoving = false; } diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index 02ba10146..ea3253d8a 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -43,15 +43,15 @@ ToggleWidget::ToggleWidget(GuiObject* boss, const GUI::Font& font, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToggleWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToggleWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 669d7341c..26922f26d 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -438,19 +438,6 @@ void Dialog::drawDialog() clearDirty(); } - Widget* w = _firstWidget; - - // Draw all children - w = _firstWidget; - while(w) - { - - // only redraw changed widgets - if(w->needsRedraw()) - w->draw(); - w = w->_next; - } - // Draw outlines for focused widgets // Don't change focus, since this will trigger lost and received // focus events @@ -458,8 +445,20 @@ void Dialog::drawDialog() { _focusedWidget = Widget::setFocusForChain(this, getFocusList(), _focusedWidget, 0, false); - if(_focusedWidget) - _focusedWidget->draw(); // make sure the highlight color is drawn initially + // if(_focusedWidget) + // _focusedWidget->draw(); // make sure the highlight color is drawn initially + } + + Widget* w = _firstWidget; + + // Draw all children + w = _firstWidget; + while(w) + { + // only redraw changed widgets + if(w->needsRedraw()) + w->draw(); + w = w->_next; } } diff --git a/src/gui/EditTextWidget.cxx b/src/gui/EditTextWidget.cxx index 6105e70d3..c818f3cf0 100644 --- a/src/gui/EditTextWidget.cxx +++ b/src/gui/EditTextWidget.cxx @@ -51,15 +51,15 @@ void EditTextWidget::setText(const string& str, bool changed) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void EditTextWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled() && isEditable()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void EditTextWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled() && isEditable()) + clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/GuiObject.hxx b/src/gui/GuiObject.hxx index a6cf4e5ca..811f1569b 100644 --- a/src/gui/GuiObject.hxx +++ b/src/gui/GuiObject.hxx @@ -97,8 +97,22 @@ class GuiObject : public CommandReceiver virtual bool isChainDirty() const = 0; virtual bool needsRedraw() { return isDirty() || isChainDirty(); }; - void setFlags(uInt32 flags) { _flags |= flags; setDirty(); } - void clearFlags(uInt32 flags) { _flags &= ~flags; setDirty(); } + void setFlags(uInt32 flags) + { + uInt32 oldFlags = _flags; + + _flags |= flags; + if(oldFlags != _flags) + setDirty(); + } + void clearFlags(uInt32 flags) + { + uInt32 oldFlags = _flags; + + _flags &= ~flags; + if(oldFlags != _flags) + setDirty(); + } uInt32 getFlags() const { return _flags; } bool hasBorder() const { return _flags & FLAG_BORDER; } diff --git a/src/gui/PopUpWidget.cxx b/src/gui/PopUpWidget.cxx index 8540f2df3..cf799dc47 100644 --- a/src/gui/PopUpWidget.cxx +++ b/src/gui/PopUpWidget.cxx @@ -161,15 +161,15 @@ void PopUpWidget::handleMouseWheel(int x, int y, int direction) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PopUpWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PopUpWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/ScrollBarWidget.cxx b/src/gui/ScrollBarWidget.cxx index 451f4a3ee..04a99c732 100644 --- a/src/gui/ScrollBarWidget.cxx +++ b/src/gui/ScrollBarWidget.cxx @@ -243,16 +243,16 @@ void ScrollBarWidget::checkBounds(int old_pos) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ScrollBarWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ScrollBarWidget::handleMouseLeft() { _part = Part::None; - clearFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/StringListWidget.cxx b/src/gui/StringListWidget.cxx index 4171632cb..5032654e6 100644 --- a/src/gui/StringListWidget.cxx +++ b/src/gui/StringListWidget.cxx @@ -53,15 +53,15 @@ void StringListWidget::setList(const StringList& list) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StringListWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StringListWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); - setDirty(); + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/TabWidget.cxx b/src/gui/TabWidget.cxx index 61f42b350..5e96338ab 100644 --- a/src/gui/TabWidget.cxx +++ b/src/gui/TabWidget.cxx @@ -216,15 +216,15 @@ void TabWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TabWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); - setDirty(); + //if(isEnabled()) + // setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TabWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); - setDirty(); + //if(isEnabled()) + // clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index c9e0441e7..69c94b3ed 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -157,6 +157,7 @@ void Widget::draw() _x = oldX; _y = oldY; } + clearDirty(); // Draw all children Widget* w = _firstWidget; @@ -166,7 +167,6 @@ void Widget::draw() w->draw(); w = w->_next; } - clearDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -448,13 +448,15 @@ ButtonWidget::ButtonWidget(GuiObject* boss, const GUI::Font& font, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ButtonWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); + if(isEnabled()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ButtonWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -558,13 +560,15 @@ CheckboxWidget::CheckboxWidget(GuiObject* boss, const GUI::Font& font, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void CheckboxWidget::handleMouseEntered() { - setFlags(Widget::FLAG_HILITED); + if(isEnabled()) + setFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void CheckboxWidget::handleMouseLeft() { - clearFlags(Widget::FLAG_HILITED); + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From cc21f75b958444ee0dc27c2d8d38251efbc0c2c8 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 11 Nov 2020 19:54:44 +0100 Subject: [PATCH 011/107] improved blinking cursor --- src/gui/EditableWidget.cxx | 52 +++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index b1783c10c..0b78c8952 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -133,7 +133,7 @@ bool EditableWidget::handleText(char text) if(tryInsertChar(text, _caretPos)) { - _caretPos++; + setCaretPos(_caretPos + 1); sendCommand(EditableWidget::kChangedCmd, 0, _id); setDirty(); return true; @@ -291,7 +291,7 @@ bool EditableWidget::handleKeyDown(StellaKey key, StellaMod mod) { // Put caret at last difference myUndoHandler->lastDiff(_editString, oldString); - _caretPos = myUndoHandler->lastDiff(_editString, oldString); + setCaretPos(myUndoHandler->lastDiff(_editString, oldString)); _selectSize = 0; sendCommand(EditableWidget::kChangedCmd, key, _id); } @@ -342,22 +342,7 @@ void EditableWidget::drawCaretSelection() if (!_editable || !isVisible() || !_boss->isVisible() || !_hasFocus) return; - if(_caretEnabled) - { - FBSurface& s = _boss->dialog().surface(); - const Common::Rect& editRect = getEditRect(); - int x = editRect.x(); - int y = editRect.y(); - x += getCaretOffset(); - - x += _x; - y += _y; - - s.vLine(x, y + 2, y + editRect.h() - 2, kTextColorHi); - s.vLine(x-1, y + 2, y + editRect.h() - 2, kTextColorHi); - clearDirty(); - } - + // Draw the selection if(_selectSize) { FBSurface& s = _boss->dialog().surface(); @@ -393,6 +378,24 @@ void EditableWidget::drawCaretSelection() s.drawString(_font, text, x, y + 1, w, h, kTextColorInv, TextAlign::Left, 0, false); } + + // Draw the caret + if(_caretEnabled ^ (_selectSize != 0)) + { + FBSurface& s = _boss->dialog().surface(); + const Common::Rect& editRect = getEditRect(); + int x = editRect.x(); + int y = editRect.y(); + ColorId color = _caretEnabled ? kTextColorHi : kTextColorInv; + + x += getCaretOffset(); + x += _x; + y += _y; + + s.vLine(x, y + 1, y + editRect.h() - 3, color); + s.vLine(x - 1, y + 1, y + editRect.h() - 3, color); + clearDirty(); + } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -401,6 +404,9 @@ bool EditableWidget::setCaretPos(int newPos) assert(newPos >= 0 && newPos <= int(_editString.size())); _caretPos = newPos; + _caretTimer = 0; + _caretEnabled = true; + return adjustOffset(); } @@ -410,6 +416,8 @@ bool EditableWidget::moveCaretPos(int direction) if(setCaretPos(_caretPos + direction)) { _selectSize -= direction; + _caretTimer = 0; + _caretEnabled = true; return true; } return false; @@ -487,6 +495,7 @@ bool EditableWidget::killChar(int direction, bool addEdit) { myUndoHandler->endChars(_editString); _editString.erase(_caretPos, 1); + setCaretPos(_caretPos); if(addEdit) myUndoHandler->doo(_editString); @@ -591,7 +600,7 @@ bool EditableWidget::moveWord(int direction, bool select) if(select) _selectSize++; } - _caretPos = currentPos; + setCaretPos(currentPos); handled = true; } else if(direction == +1) // move to first character of next word @@ -610,7 +619,7 @@ bool EditableWidget::moveWord(int direction, bool select) if(select) _selectSize--; } - _caretPos = currentPos; + setCaretPos(currentPos); handled = true; } @@ -665,6 +674,7 @@ bool EditableWidget::killSelectedText(bool addEdit) _selectSize = -_selectSize; } _editString.erase(_caretPos, _selectSize); + setCaretPos(_caretPos); _selectSize = 0; if(addEdit) myUndoHandler->doo(_editString); @@ -724,7 +734,7 @@ bool EditableWidget::pasteSelectedText() _editString.insert(_caretPos, buf.str()); // position cursor at the end of pasted text - _caretPos += int(buf.str().length()); + setCaretPos(_caretPos + int(buf.str().length())); if(selected || !pasted.empty()) { From f64285425ac7f6bccbf39cb32f69056150f4865e Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 11 Nov 2020 23:32:00 +0100 Subject: [PATCH 012/107] split Dialog drawing and rendering and skip drawing render when possible --- src/gui/Dialog.cxx | 17 ++++++++++------- src/gui/Dialog.hxx | 3 ++- src/gui/DialogContainer.cxx | 20 ++++++++++---------- src/gui/DialogContainer.hxx | 4 +--- src/gui/EditableWidget.cxx | 1 - src/gui/PopUpWidget.cxx | 2 +- 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 26922f26d..9cb5accb2 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -128,7 +128,6 @@ void Dialog::close() _visible = false; parent().removeDialog(); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -219,26 +218,30 @@ void Dialog::positionAt(uInt32 pos) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool Dialog::render() +void Dialog::redraw() { if(!isVisible() || !needsRedraw()) - return false; + return;// false; // Draw this dialog center(); drawDialog(); + render(); +// return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Dialog::render() +{ // Update dialog surface; also render any extra surfaces // Extra surfaces must be rendered afterwards, so they are drawn on top if(_surface->render()) { - mySurfaceStack.applyAll([](shared_ptr& surface){ + mySurfaceStack.applyAll([](shared_ptr& surface) { surface->render(); }); } - //_dirty = false; - - return true; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index 68a71608c..b9a924b55 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -67,7 +67,8 @@ class Dialog : public GuiObject void setDirty() override; bool isDirty() override; // TODO: remove bool isChainDirty() const override; - bool render(); + void redraw(); + void render(); void addFocusWidget(Widget* w) override; void addToFocusList(WidgetArray& list) override; diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 60bc9b32d..9649e447c 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -89,11 +89,11 @@ void DialogContainer::updateTime(uInt64 time) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool DialogContainer::draw(bool full) +void DialogContainer::draw(bool full) { cerr << "draw " << full << " " << typeid(*this).name() << endl; if(myDialogStack.empty()) - return false; + return; // Make the top dialog dirty if a full redraw is requested if(full) @@ -102,10 +102,8 @@ bool DialogContainer::draw(bool full) // Render all dirty dialogs myDialogStack.applyAll([&](Dialog*& d) { if(d->needsRedraw()) - full |= d->render(); + d->redraw(); }); - - return full; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -133,7 +131,7 @@ int DialogContainer::addDialog(Dialog* d) "Unable to show dialog box; FIX THE CODE"); else { - // fade out current top dialog + // "darken" current top dialog if(!myDialogStack.empty()) myDialogStack.top()->setDirty(); d->setDirty(); @@ -148,14 +146,16 @@ void DialogContainer::removeDialog() if(!myDialogStack.empty()) { myDialogStack.pop(); - // necessary as long as all dialogs share the same surface + if(!myDialogStack.empty()) { - //myDialogStack.top()->setDirty(); + // this "undarkens" the top dialog + myDialogStack.top()->setDirty(); - // Mark all dialogs for redraw + // Rerender all dialogs (TODO: top dialog is rendered twice) myDialogStack.applyAll([&](Dialog*& d){ - d->setDirty(); + //d->setDirty(); + d->render(); }); } } diff --git a/src/gui/DialogContainer.hxx b/src/gui/DialogContainer.hxx index cc50e23b6..1f6cce8f6 100644 --- a/src/gui/DialogContainer.hxx +++ b/src/gui/DialogContainer.hxx @@ -121,10 +121,8 @@ class DialogContainer /** Draw the stack of menus (full indicates to redraw all items). - - @return Answers whether any drawing actually occurred. */ - bool draw(bool full = false); + void draw(bool full = false); /** Answers whether a full redraw is required. diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index 0b78c8952..4032daae3 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -74,7 +74,6 @@ bool EditableWidget::isDirty() _caretEnabled = !_caretEnabled; _dirty = true; } - cerr << "."; } return _dirty; diff --git a/src/gui/PopUpWidget.cxx b/src/gui/PopUpWidget.cxx index cf799dc47..ccf6af1aa 100644 --- a/src/gui/PopUpWidget.cxx +++ b/src/gui/PopUpWidget.cxx @@ -277,7 +277,7 @@ void PopUpWidget::drawWidget(bool hilite) // Fill the background ColorId bgCol = isEditable() ? kWidColor : kDlgColor; - s.fillRect(x + 1, _y + 1, w - (_arrowWidth * 2 - 0), _h - 2, + s.fillRect(x + 1, _y + 1, w - (_arrowWidth * 2 - 1), _h - 2, onTop ? _changed ? kDbgChangedColor : bgCol : kDlgColor); s.fillRect(x + w - (_arrowWidth * 2 - 2), _y + 1, (_arrowWidth * 2 - 3), _h - 2, onTop ? isEnabled() && hilite ? kBtnColorHi : bgCol : kBGColorLo); From a643b3d239dbe5af0464b3da53cde0ac56d9217d Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 12 Nov 2020 10:43:04 +0100 Subject: [PATCH 013/107] minimized UI redraws and renderings when message is displayed refactored message creation --- src/common/PNGLibrary.cxx | 6 +- src/common/PaletteHandler.cxx | 8 +- src/common/RewindManager.cxx | 4 +- src/common/SoundSDL2.cxx | 4 +- src/common/StateManager.cxx | 18 +- src/debugger/gui/RomWidget.cxx | 2 +- src/debugger/gui/TiaOutputWidget.cxx | 8 +- src/debugger/gui/TiaZoomWidget.cxx | 4 +- src/emucore/Console.cxx | 42 ++-- src/emucore/EventHandler.cxx | 46 ++--- src/emucore/FrameBuffer.cxx | 295 +++++++++++++-------------- src/emucore/FrameBuffer.hxx | 25 ++- src/emucore/OSystem.cxx | 6 +- src/emucore/QuadTari.cxx | 2 +- src/emucore/TIASurface.cxx | 10 +- src/gui/DialogContainer.cxx | 4 +- src/gui/GameInfoDialog.cxx | 6 +- src/gui/LauncherDialog.cxx | 2 +- src/gui/LoggerDialog.cxx | 4 +- src/gui/TimeMachineDialog.cxx | 4 +- 20 files changed, 252 insertions(+), 248 deletions(-) diff --git a/src/common/PNGLibrary.cxx b/src/common/PNGLibrary.cxx index 901d42478..2a6f84d6a 100644 --- a/src/common/PNGLibrary.cxx +++ b/src/common/PNGLibrary.cxx @@ -267,7 +267,7 @@ void PNGLibrary::toggleContinuousSnapshots(bool perFrame) buf << "Enabling snapshots in " << interval << " second intervals"; interval *= uInt32(myOSystem.frameRate()); } - myOSystem.frameBuffer().showMessage(buf.str()); + myOSystem.frameBuffer().showTextMessage(buf.str()); setContinuousSnapInterval(interval); } else @@ -276,7 +276,7 @@ void PNGLibrary::toggleContinuousSnapshots(bool perFrame) buf << "Disabling snapshots, generated " << (mySnapCounter / mySnapInterval) << " files"; - myOSystem.frameBuffer().showMessage(buf.str()); + myOSystem.frameBuffer().showTextMessage(buf.str()); setContinuousSnapInterval(0); } } @@ -378,7 +378,7 @@ void PNGLibrary::takeSnapshot(uInt32 number) // Re-enable old messages myOSystem.frameBuffer().enableMessages(true); } - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/common/PaletteHandler.cxx b/src/common/PaletteHandler.cxx index db6961af8..f13c18ab1 100644 --- a/src/common/PaletteHandler.cxx +++ b/src/common/PaletteHandler.cxx @@ -69,7 +69,7 @@ void PaletteHandler::cyclePalette(int direction) const string palette = toPaletteName(PaletteType(type)); const string message = MESSAGES[type] + " palette"; - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); setPalette(palette); } @@ -112,7 +112,7 @@ void PaletteHandler::showAdjustableMessage() const float value = myOSystem.console().timing() == ConsoleTiming::pal ? myPhasePAL : myPhaseNTSC; buf << std::fixed << std::setprecision(1) << value << DEGREE; - myOSystem.frameBuffer().showMessage( + myOSystem.frameBuffer().showGaugeMessage( "Palette phase shift", buf.str(), value, (isNTSC ? DEF_NTSC_SHIFT : DEF_PAL_SHIFT) - MAX_PHASE_SHIFT, (isNTSC ? DEF_NTSC_SHIFT : DEF_PAL_SHIFT) + MAX_PHASE_SHIFT); @@ -122,7 +122,7 @@ void PaletteHandler::showAdjustableMessage() const float value = *myAdjustables[myCurrentAdjustable].value; buf << std::fixed << std::setprecision(1) << value << DEGREE; - myOSystem.frameBuffer().showMessage( + myOSystem.frameBuffer().showGaugeMessage( msg.str(), buf.str(), value, -MAX_RGB_SHIFT, +MAX_RGB_SHIFT); } else @@ -131,7 +131,7 @@ void PaletteHandler::showAdjustableMessage() ? scaleRGBTo100(*myAdjustables[myCurrentAdjustable].value) : scaleTo100(*myAdjustables[myCurrentAdjustable].value); buf << value << "%"; - myOSystem.frameBuffer().showMessage( + myOSystem.frameBuffer().showGaugeMessage( msg.str(), buf.str(), value); } } diff --git a/src/common/RewindManager.cxx b/src/common/RewindManager.cxx index bc6b70813..044bc57e9 100644 --- a/src/common/RewindManager.cxx +++ b/src/common/RewindManager.cxx @@ -181,7 +181,7 @@ uInt32 RewindManager::rewindStates(uInt32 numStates) if(myOSystem.eventHandler().state() != EventHandlerState::TIMEMACHINE && myOSystem.eventHandler().state() != EventHandlerState::PLAYBACK) - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); return i; } @@ -216,7 +216,7 @@ uInt32 RewindManager::unwindStates(uInt32 numStates) if(myOSystem.eventHandler().state() != EventHandlerState::TIMEMACHINE && myOSystem.eventHandler().state() != EventHandlerState::PLAYBACK) - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); return i; } diff --git a/src/common/SoundSDL2.cxx b/src/common/SoundSDL2.cxx index 6caabc72e..40b95b0f0 100644 --- a/src/common/SoundSDL2.cxx +++ b/src/common/SoundSDL2.cxx @@ -224,7 +224,7 @@ bool SoundSDL2::toggleMute() string message = "Sound "; message += enabled ? "unmuted" : "muted"; - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); //ostringstream strval; //uInt32 volume; @@ -282,7 +282,7 @@ void SoundSDL2::adjustVolume(int direction) strval << percent << "%"; else strval << "Off"; - myOSystem.frameBuffer().showMessage("Volume", strval.str(), percent); + myOSystem.frameBuffer().showGaugeMessage("Volume", strval.str(), percent); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/common/StateManager.cxx b/src/common/StateManager.cxx index a4add0889..956f62409 100644 --- a/src/common/StateManager.cxx +++ b/src/common/StateManager.cxx @@ -132,9 +132,9 @@ void StateManager::toggleTimeMachine() myActiveMode = myActiveMode == Mode::TimeMachine ? Mode::Off : Mode::TimeMachine; if(myActiveMode == Mode::TimeMachine) - myOSystem.frameBuffer().showMessage("Time Machine enabled"); + myOSystem.frameBuffer().showTextMessage("Time Machine enabled"); else - myOSystem.frameBuffer().showMessage("Time Machine disabled"); + myOSystem.frameBuffer().showTextMessage("Time Machine disabled"); myOSystem.settings().setValue(devSettings ? "dev.timemachine" : "plr.timemachine", myActiveMode == Mode::TimeMachine); } @@ -215,7 +215,7 @@ void StateManager::loadState(int slot) { buf.str(""); buf << "Can't open/load from state file " << slot; - myOSystem.frameBuffer().showMessage(buf.str()); + myOSystem.frameBuffer().showTextMessage(buf.str()); return; } @@ -239,7 +239,7 @@ void StateManager::loadState(int slot) buf << "Invalid data in state " << slot << " file"; } - myOSystem.frameBuffer().showMessage(buf.str()); + myOSystem.frameBuffer().showTextMessage(buf.str()); } } @@ -261,7 +261,7 @@ void StateManager::saveState(int slot) { buf.str(""); buf << "Can't open/save to state file " << slot; - myOSystem.frameBuffer().showMessage(buf.str()); + myOSystem.frameBuffer().showTextMessage(buf.str()); return; } @@ -274,7 +274,7 @@ void StateManager::saveState(int slot) catch(...) { buf << "Error saving state " << slot; - myOSystem.frameBuffer().showMessage(buf.str()); + myOSystem.frameBuffer().showTextMessage(buf.str()); return; } @@ -292,7 +292,7 @@ void StateManager::saveState(int slot) else buf << "Error saving state " << slot; - myOSystem.frameBuffer().showMessage(buf.str()); + myOSystem.frameBuffer().showTextMessage(buf.str()); } } @@ -307,7 +307,7 @@ void StateManager::changeState(int direction) buf << "Changed to state slot " << myCurrentSlot; else buf << "State slot " << myCurrentSlot; - myOSystem.frameBuffer().showMessage(buf.str()); + myOSystem.frameBuffer().showTextMessage(buf.str()); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -318,7 +318,7 @@ void StateManager::toggleAutoSlot() // Print appropriate message ostringstream buf; buf << "Automatic slot change " << (autoSlot ? "enabled" : "disabled"); - myOSystem.frameBuffer().showMessage(buf.str()); + myOSystem.frameBuffer().showTextMessage(buf.str()); myOSystem.settings().setValue("autoslot", autoSlot); } diff --git a/src/debugger/gui/RomWidget.cxx b/src/debugger/gui/RomWidget.cxx index 9b387d69f..87572d6ed 100644 --- a/src/debugger/gui/RomWidget.cxx +++ b/src/debugger/gui/RomWidget.cxx @@ -199,7 +199,7 @@ void RomWidget::runtoPC(int disasm_line) ostringstream command; command << "runtopc #" << address; string msg = instance().debugger().run(command.str()); - instance().frameBuffer().showMessage(msg); + instance().frameBuffer().showTextMessage(msg); } } diff --git a/src/debugger/gui/TiaOutputWidget.cxx b/src/debugger/gui/TiaOutputWidget.cxx index 01d6d094b..2062bd436 100644 --- a/src/debugger/gui/TiaOutputWidget.cxx +++ b/src/debugger/gui/TiaOutputWidget.cxx @@ -92,10 +92,10 @@ void TiaOutputWidget::saveSnapshot(int execDepth, const string& execPrefix) message = e.what(); } if (execDepth == 0) { - instance().frameBuffer().showMessage(message); + instance().frameBuffer().showTextMessage(message); } #else - instance().frameBuffer().showMessage("PNG image saving not supported"); + instance().frameBuffer().showTextMessage("PNG image saving not supported"); #endif } @@ -135,7 +135,7 @@ void TiaOutputWidget::handleCommand(CommandSender* sender, int cmd, int data, in { command << "scanline #" << lines; string message = instance().debugger().parser().run(command.str()); - instance().frameBuffer().showMessage(message); + instance().frameBuffer().showTextMessage(message); } } else if(rmb == "bp") @@ -144,7 +144,7 @@ void TiaOutputWidget::handleCommand(CommandSender* sender, int cmd, int data, in int scanline = myClickY + startLine; command << "breakif _scan==#" << scanline; string message = instance().debugger().parser().run(command.str()); - instance().frameBuffer().showMessage(message); + instance().frameBuffer().showTextMessage(message); } else if(rmb == "zoom") { diff --git a/src/debugger/gui/TiaZoomWidget.cxx b/src/debugger/gui/TiaZoomWidget.cxx index a008c4444..759e95516 100644 --- a/src/debugger/gui/TiaZoomWidget.cxx +++ b/src/debugger/gui/TiaZoomWidget.cxx @@ -262,7 +262,7 @@ void TiaZoomWidget::handleCommand(CommandSender* sender, int cmd, int data, int { command << "scanline #" << lines; string message = instance().debugger().parser().run(command.str()); - instance().frameBuffer().showMessage(message); + instance().frameBuffer().showTextMessage(message); } } else if(rmb == "bp") @@ -271,7 +271,7 @@ void TiaZoomWidget::handleCommand(CommandSender* sender, int cmd, int data, int int scanline = myClickY / myZoomLevel + myOffY + startLine; command << "breakif _scan==#" << scanline; string message = instance().debugger().parser().run(command.str()); - instance().frameBuffer().showMessage(message); + instance().frameBuffer().showTextMessage(message); } else { diff --git a/src/emucore/Console.cxx b/src/emucore/Console.cxx index 099773989..bb292859e 100644 --- a/src/emucore/Console.cxx +++ b/src/emucore/Console.cxx @@ -476,7 +476,7 @@ void Console::setFormat(uInt32 format, bool force) initializeAudio(); // ensure that audio synthesis is set up to match emulation rate myOSystem.resetFps(); // Reset FPS measurement - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); // Let the other devices know about the console change mySystem->consoleChanged(myConsoleTiming); @@ -493,10 +493,10 @@ void Console::toggleColorLoss(bool toggle) string message = string("PAL color-loss ") + (colorloss ? "enabled" : "disabled"); - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); } else - myOSystem.frameBuffer().showMessage( + myOSystem.frameBuffer().showTextMessage( "PAL color-loss not available in non PAL modes"); } @@ -521,7 +521,7 @@ void Console::toggleInter(bool toggle) ostringstream ss; ss << "Interpolation " << (enabled ? "enabled" : "disabled"); - myOSystem.frameBuffer().showMessage(ss.str()); + myOSystem.frameBuffer().showTextMessage(ss.str()); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -539,7 +539,7 @@ void Console::toggleTurbo() ostringstream ss; ss << "Turbo mode " << (!enabled ? "enabled" : "disabled"); - myOSystem.frameBuffer().showMessage(ss.str()); + myOSystem.frameBuffer().showTextMessage(ss.str()); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -564,7 +564,7 @@ void Console::changeSpeed(int direction) ostringstream val; val << formatSpeed(speed) << "%"; - myOSystem.frameBuffer().showMessage("Emulation speed", val.str(), speed, MIN_SPEED, MAX_SPEED); + myOSystem.frameBuffer().showGaugeMessage("Emulation speed", val.str(), speed, MIN_SPEED, MAX_SPEED); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -574,13 +574,13 @@ void Console::togglePhosphor() { myProperties.set(PropType::Display_Phosphor, "NO"); myOSystem.frameBuffer().tiaSurface().enablePhosphor(false); - myOSystem.frameBuffer().showMessage("Phosphor effect disabled"); + myOSystem.frameBuffer().showTextMessage("Phosphor effect disabled"); } else { myProperties.set(PropType::Display_Phosphor, "YES"); myOSystem.frameBuffer().tiaSurface().enablePhosphor(true); - myOSystem.frameBuffer().showMessage("Phosphor effect enabled"); + myOSystem.frameBuffer().showTextMessage("Phosphor effect enabled"); } } @@ -605,7 +605,7 @@ void Console::changePhosphor(int direction) val.str(""); val << "Off"; } - myOSystem.frameBuffer().showMessage("Phosphor blend", val.str(), blend); + myOSystem.frameBuffer().showGaugeMessage("Phosphor blend", val.str(), blend); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -699,7 +699,7 @@ void Console::changeVerticalCenter(int direction) if (vcenter != myTIA->vcenter()) myTIA->setVcenter(vcenter); val << (vcenter ? vcenter > 0 ? "+" : "" : " ") << vcenter << "px"; - myOSystem.frameBuffer().showMessage("V-Center", val.str(), vcenter, + myOSystem.frameBuffer().showGaugeMessage("V-Center", val.str(), vcenter, myTIA->minVcenter(), myTIA->maxVcenter()); } @@ -729,7 +729,7 @@ void Console::changeVSizeAdjust(int direction) val << (newAdjustVSize ? newAdjustVSize > 0 ? "+" : "" : " ") << newAdjustVSize << "%"; - myOSystem.frameBuffer().showMessage("V-Size", val.str(), newAdjustVSize, -5, 5); + myOSystem.frameBuffer().showGaugeMessage("V-Size", val.str(), newAdjustVSize, -5, 5); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -746,7 +746,7 @@ void Console::toggleCorrectAspectRatio(bool toggle) const string& message = string("Correct aspect ratio ") + (enabled ? "enabled" : "disabled"); - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -920,7 +920,7 @@ unique_ptr Console::getControllerPort(const Controller::Type type, Controller::onMessageCallback callback = [&os = myOSystem](const string& msg) { bool devSettings = os.settings().getBool("dev.settings"); if(os.settings().getBool(devSettings ? "dev.eepromaccess" : "plr.eepromaccess")) - os.frameBuffer().showMessage(msg); + os.frameBuffer().showTextMessage(msg); }; controller = make_unique(port, myEvent, *mySystem, myOSystem.settings().getString("avoxport"), nvramfile, callback); @@ -933,7 +933,7 @@ unique_ptr Console::getControllerPort(const Controller::Type type, Controller::onMessageCallback callback = [&os = myOSystem](const string& msg) { bool devSettings = os.settings().getBool("dev.settings"); if(os.settings().getBool(devSettings ? "dev.eepromaccess" : "plr.eepromaccess")) - os.frameBuffer().showMessage(msg); + os.frameBuffer().showTextMessage(msg); }; controller = make_unique(port, myEvent, *mySystem, nvramfile, callback); break; @@ -987,7 +987,7 @@ void Console::changeAutoFireRate(int direction) else val << "Off"; - myOSystem.frameBuffer().showMessage("Autofire rate", val.str(), rate, 0, isNTSC ? 30 : 25); + myOSystem.frameBuffer().showGaugeMessage("Autofire rate", val.str(), rate, 0, isNTSC ? 30 : 25); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1012,7 +1012,7 @@ void Console::toggleTIABit(TIABit bit, const string& bitname, bool show, bool to bool result = myTIA->toggleBit(bit, toggle ? 2 : 3); const string message = bitname + (result ? " enabled" : " disabled"); - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1021,7 +1021,7 @@ void Console::toggleBits(bool toggle) const bool enabled = myTIA->toggleBits(toggle); const string message = string("TIA bits ") + (enabled ? "enabled" : "disabled"); - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1030,7 +1030,7 @@ void Console::toggleTIACollision(TIABit bit, const string& bitname, bool show, b bool result = myTIA->toggleCollision(bit, toggle ? 2 : 3); const string message = bitname + (result ? " collision enabled" : " collision disabled"); - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1039,7 +1039,7 @@ void Console::toggleCollisions(bool toggle) const bool enabled = myTIA->toggleCollisions(toggle); const string message = string("TIA collisions ") + (enabled ? "enabled" : "disabled"); - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1048,7 +1048,7 @@ void Console::toggleFixedColors(bool toggle) const bool enabled = toggle ? myTIA->toggleFixedColors() : myTIA->usingFixedColors(); const string message = string("Fixed debug colors ") + (enabled ? "enabled" : "disabled"); - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1057,7 +1057,7 @@ void Console::toggleJitter(bool toggle) const bool enabled = myTIA->toggleJitter(toggle ? 2 : 3); const string message = string("TV scanline jitter ") + (enabled ? "enabled" : "disabled"); - myOSystem.frameBuffer().showMessage(message); + myOSystem.frameBuffer().showTextMessage(message); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/emucore/EventHandler.cxx b/src/emucore/EventHandler.cxx index 871d6cd73..9e7c9ea5f 100644 --- a/src/emucore/EventHandler.cxx +++ b/src/emucore/EventHandler.cxx @@ -191,12 +191,12 @@ void EventHandler::toggleSAPortOrder() if(saport == "lr") { mapStelladaptors("rl"); - myOSystem.frameBuffer().showMessage("Stelladaptor ports right/left"); + myOSystem.frameBuffer().showTextMessage("Stelladaptor ports right/left"); } else { mapStelladaptors("lr"); - myOSystem.frameBuffer().showMessage("Stelladaptor ports left/right"); + myOSystem.frameBuffer().showTextMessage("Stelladaptor ports left/right"); } #endif } @@ -214,7 +214,7 @@ void EventHandler::set7800Mode() void EventHandler::handleMouseControl() { if(myMouseControl) - myOSystem.frameBuffer().showMessage(myMouseControl->next()); + myOSystem.frameBuffer().showTextMessage(myMouseControl->next()); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -550,7 +550,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) default: break; } - myOSystem.frameBuffer().showMessage(msg + " settings"); + myOSystem.frameBuffer().showTextMessage(msg + " settings"); myAdjustActive = false; } break; @@ -1210,7 +1210,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) case Event::SaveAllStates: if (pressed && !repeated) - myOSystem.frameBuffer().showMessage(myOSystem.state().rewindManager().saveAllStates()); + myOSystem.frameBuffer().showTextMessage(myOSystem.state().rewindManager().saveAllStates()); return; case Event::PreviousState: @@ -1243,7 +1243,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) case Event::LoadAllStates: if (pressed && !repeated) - myOSystem.frameBuffer().showMessage(myOSystem.state().rewindManager().loadAllStates()); + myOSystem.frameBuffer().showTextMessage(myOSystem.state().rewindManager().loadAllStates()); return; case Event::RewindPause: @@ -1476,7 +1476,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) { myEvent.set(Event::ConsoleBlackWhite, 0); myEvent.set(Event::ConsoleColor, 1); - myOSystem.frameBuffer().showMessage(myIs7800 ? "Pause released" : "Color Mode"); + myOSystem.frameBuffer().showTextMessage(myIs7800 ? "Pause released" : "Color Mode"); myOSystem.console().switches().update(); } return; @@ -1485,7 +1485,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) { myEvent.set(Event::ConsoleBlackWhite, 1); myEvent.set(Event::ConsoleColor, 0); - myOSystem.frameBuffer().showMessage(myIs7800 ? "Pause pushed" : "B/W Mode"); + myOSystem.frameBuffer().showTextMessage(myIs7800 ? "Pause pushed" : "B/W Mode"); myOSystem.console().switches().update(); } return; @@ -1496,13 +1496,13 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) { myEvent.set(Event::ConsoleBlackWhite, 1); myEvent.set(Event::ConsoleColor, 0); - myOSystem.frameBuffer().showMessage(myIs7800 ? "Pause pushed" : "B/W Mode"); + myOSystem.frameBuffer().showTextMessage(myIs7800 ? "Pause pushed" : "B/W Mode"); } else { myEvent.set(Event::ConsoleBlackWhite, 0); myEvent.set(Event::ConsoleColor, 1); - myOSystem.frameBuffer().showMessage(myIs7800 ? "Pause released" : "Color Mode"); + myOSystem.frameBuffer().showTextMessage(myIs7800 ? "Pause released" : "Color Mode"); } myOSystem.console().switches().update(); } @@ -1514,7 +1514,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) myEvent.set(Event::ConsoleBlackWhite, 0); myEvent.set(Event::ConsoleColor, 0); if (myIs7800) - myOSystem.frameBuffer().showMessage("Pause pressed"); + myOSystem.frameBuffer().showTextMessage("Pause pressed"); myOSystem.console().switches().update(); } return; @@ -1524,7 +1524,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) { myEvent.set(Event::ConsoleLeftDiffA, 1); myEvent.set(Event::ConsoleLeftDiffB, 0); - myOSystem.frameBuffer().showMessage(GUI::LEFT_DIFFICULTY + " A"); + myOSystem.frameBuffer().showTextMessage(GUI::LEFT_DIFFICULTY + " A"); myOSystem.console().switches().update(); } return; @@ -1533,7 +1533,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) { myEvent.set(Event::ConsoleLeftDiffA, 0); myEvent.set(Event::ConsoleLeftDiffB, 1); - myOSystem.frameBuffer().showMessage(GUI::LEFT_DIFFICULTY + " B"); + myOSystem.frameBuffer().showTextMessage(GUI::LEFT_DIFFICULTY + " B"); myOSystem.console().switches().update(); } return; @@ -1544,13 +1544,13 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) { myEvent.set(Event::ConsoleLeftDiffA, 0); myEvent.set(Event::ConsoleLeftDiffB, 1); - myOSystem.frameBuffer().showMessage(GUI::LEFT_DIFFICULTY + " B"); + myOSystem.frameBuffer().showTextMessage(GUI::LEFT_DIFFICULTY + " B"); } else { myEvent.set(Event::ConsoleLeftDiffA, 1); myEvent.set(Event::ConsoleLeftDiffB, 0); - myOSystem.frameBuffer().showMessage(GUI::LEFT_DIFFICULTY + " A"); + myOSystem.frameBuffer().showTextMessage(GUI::LEFT_DIFFICULTY + " A"); } myOSystem.console().switches().update(); } @@ -1561,7 +1561,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) { myEvent.set(Event::ConsoleRightDiffA, 1); myEvent.set(Event::ConsoleRightDiffB, 0); - myOSystem.frameBuffer().showMessage(GUI::RIGHT_DIFFICULTY + " A"); + myOSystem.frameBuffer().showTextMessage(GUI::RIGHT_DIFFICULTY + " A"); myOSystem.console().switches().update(); } return; @@ -1570,7 +1570,7 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) { myEvent.set(Event::ConsoleRightDiffA, 0); myEvent.set(Event::ConsoleRightDiffB, 1); - myOSystem.frameBuffer().showMessage(GUI::RIGHT_DIFFICULTY + " B"); + myOSystem.frameBuffer().showTextMessage(GUI::RIGHT_DIFFICULTY + " B"); myOSystem.console().switches().update(); } return; @@ -1581,13 +1581,13 @@ void EventHandler::handleEvent(Event::Type event, Int32 value, bool repeated) { myEvent.set(Event::ConsoleRightDiffA, 0); myEvent.set(Event::ConsoleRightDiffB, 1); - myOSystem.frameBuffer().showMessage(GUI::RIGHT_DIFFICULTY + " B"); + myOSystem.frameBuffer().showTextMessage(GUI::RIGHT_DIFFICULTY + " B"); } else { myEvent.set(Event::ConsoleRightDiffA, 1); myEvent.set(Event::ConsoleRightDiffB, 0); - myOSystem.frameBuffer().showMessage(GUI::RIGHT_DIFFICULTY + " A"); + myOSystem.frameBuffer().showTextMessage(GUI::RIGHT_DIFFICULTY + " A"); } myOSystem.console().switches().update(); } @@ -2312,15 +2312,15 @@ bool EventHandler::enterDebugMode() myOSystem.debugger().setQuitState(); setState(EventHandlerState::EMULATION); if(fbstatus == FBInitStatus::FailTooLarge) - myOSystem.frameBuffer().showMessage("Debugger window too large for screen", - MessagePosition::BottomCenter, true); + myOSystem.frameBuffer().showTextMessage("Debugger window too large for screen", + MessagePosition::BottomCenter, true); return false; } myOverlay->reStack(); myOSystem.sound().mute(true); #else - myOSystem.frameBuffer().showMessage("Debugger support not included", - MessagePosition::BottomCenter, true); + myOSystem.frameBuffer().showTextMessage("Debugger support not included", + MessagePosition::BottomCenter, true); #endif return true; diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index 828c14e8f..66747b2a3 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -322,13 +322,8 @@ void FrameBuffer::update(bool force) // - at the bottom of ::update(), to actually draw them (this must come // last, since they are always drawn on top of everything else). - // Full rendering is required when messages are enabled - force |= (myMsg.counter >= 0); - - // Detect when a message has been turned off; one last redraw is required - // in this case, to draw over the area that the message occupied - if(myMsg.counter == 0) - myMsg.counter = -1; + // Forced full rendering is required when messages are being disabled + force |= (myMsg.enabled && myMsg.counter == 0); bool redraw = false; switch(myOSystem.eventHandler().state()) @@ -344,7 +339,7 @@ void FrameBuffer::update(bool force) if(myPausedCount-- <= 0) { myPausedCount = uInt32(7 * myOSystem.frameRate()); - showMessage("Paused", MessagePosition::MiddleCenter); + showTextMessage("Paused", MessagePosition::MiddleCenter); } if(force) myTIASurface->render(); @@ -422,12 +417,12 @@ void FrameBuffer::update(bool force) } force = force || success; - if (force) + if(force) myTIASurface->render(); // Stop playback mode at the end of the state buffer // and switch to Time Machine or Pause mode - if (!success) + if(!success) { frames = 0; myOSystem.eventHandler().enterMenuMode(EventHandlerState::TIMEMACHINE); @@ -468,7 +463,7 @@ void FrameBuffer::update(bool force) // indicates that, and then the code at the top of this method sees // the change and redraws everything if(myMsg.enabled) - drawMessage(); + redraw |= drawMessage(); // Push buffers to screen only when necessary if(force || redraw) @@ -501,19 +496,15 @@ void FrameBuffer::updateInEmulationMode(float framesPerSecond) myBackend->renderToScreen(); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void FrameBuffer::showMessage(const string& message, MessagePosition position, - bool force) -{ #ifdef GUI_SUPPORT +void FrameBuffer::createMessage(const string& message, MessagePosition position, bool force) +{ // Only show messages if they've been enabled if(myMsg.surface == nullptr || !(force || myOSystem.settings().getBool("uimessages"))) return; - const int fontWidth = font().getMaxCharWidth(), - fontHeight = font().getFontHeight(); + const int fontHeight = font().getFontHeight(); const int VBORDER = fontHeight / 4; - const int HBORDER = fontWidth * 1.25 / 2.0; myMsg.counter = uInt32(myOSystem.frameRate()) * 2; // Show message for 2 seconds if(myMsg.counter == 0) @@ -522,39 +513,41 @@ void FrameBuffer::showMessage(const string& message, MessagePosition position, // Precompute the message coordinates myMsg.text = message; myMsg.color = kBtnTextColor; - myMsg.showGauge = false; - myMsg.w = std::min(fontWidth * (MESSAGE_WIDTH) - HBORDER * 2, - font().getStringWidth(myMsg.text) + HBORDER * 2); myMsg.h = fontHeight + VBORDER * 2; myMsg.position = position; myMsg.enabled = true; + myMsg.dirty = true; + myMsg.surface->setSrcSize(myMsg.w, myMsg.h); myMsg.surface->setDstSize(myMsg.w * hidpiScaleFactor(), myMsg.h * hidpiScaleFactor()); +} +#endif + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void FrameBuffer::showTextMessage(const string& message, MessagePosition position, + bool force) +{ +#ifdef GUI_SUPPORT + const int fontWidth = font().getMaxCharWidth(); + const int HBORDER = fontWidth * 1.25 / 2.0; + + myMsg.showGauge = false; + myMsg.w = std::min(fontWidth * (MESSAGE_WIDTH) - HBORDER * 2, + font().getStringWidth(message) + HBORDER * 2); + + createMessage(message, position, force); #endif } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void FrameBuffer::showMessage(const string& message, const string& valueText, - float value, float minValue, float maxValue) +void FrameBuffer::showGaugeMessage(const string& message, const string& valueText, + float value, float minValue, float maxValue) { #ifdef GUI_SUPPORT - // Only show messages if they've been enabled - if(myMsg.surface == nullptr || !myOSystem.settings().getBool("uimessages")) - return; - - const int fontWidth = font().getMaxCharWidth(), - fontHeight = font().getFontHeight(); - const int VBORDER = fontHeight / 4; + const int fontWidth = font().getMaxCharWidth(); const int HBORDER = fontWidth * 1.25 / 2.0; - myMsg.counter = uInt32(myOSystem.frameRate()) * 2; // Show message for 2 seconds - if(myMsg.counter == 0) - myMsg.counter = 120; - - // Precompute the message coordinates - myMsg.text = message; - myMsg.color = kBtnTextColor; myMsg.showGauge = true; if(maxValue - minValue != 0) myMsg.value = (value - minValue) / (maxValue - minValue) * 100.F; @@ -562,16 +555,12 @@ void FrameBuffer::showMessage(const string& message, const string& valueText, myMsg.value = 100.F; myMsg.valueText = valueText; myMsg.w = std::min(fontWidth * MESSAGE_WIDTH, - font().getStringWidth(myMsg.text) + font().getStringWidth(message) + fontWidth * (GAUGEBAR_WIDTH + 2) - + font().getStringWidth(myMsg.valueText)) - + HBORDER * 2; - myMsg.h = fontHeight + VBORDER * 2; - myMsg.position = MessagePosition::BottomCenter; - myMsg.enabled = true; + + font().getStringWidth(valueText)) + + HBORDER * 2; - myMsg.surface->setSrcSize(myMsg.w, myMsg.h); - myMsg.surface->setDstSize(myMsg.w * hidpiScaleFactor(), myMsg.h * hidpiScaleFactor()); + createMessage(message, MessagePosition::BottomCenter); #endif } @@ -652,8 +641,8 @@ void FrameBuffer::toggleFrameStats(bool toggle) myOSystem.settings().setValue( myOSystem.settings().getBool("dev.settings") ? "dev.stats" : "plr.stats", myStatsEnabled); - myOSystem.frameBuffer().showMessage(string("Console info ") + - (myStatsEnabled ? "enabled" : "disabled")); + myOSystem.frameBuffer().showTextMessage(string("Console info ") + + (myStatsEnabled ? "enabled" : "disabled")); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -676,7 +665,6 @@ void FrameBuffer::enableMessages(bool enable) myStatsMsg.enabled = false; // Erase old messages on the screen - myMsg.enabled = false; myMsg.counter = 0; update(true); // Force update immediately } @@ -689,116 +677,120 @@ inline bool FrameBuffer::drawMessage() // Either erase the entire message (when time is reached), // or show again this frame if(myMsg.counter == 0) - { - myMsg.enabled = false; - return true; - } - else if(myMsg.counter < 0) { myMsg.enabled = false; return false; } - // Draw the bounded box and text - const Common::Rect& dst = myMsg.surface->dstRect(); - const int fontWidth = font().getMaxCharWidth(), - fontHeight = font().getFontHeight(); - const int VBORDER = fontHeight / 4; - const int HBORDER = fontWidth * 1.25 / 2.0; - constexpr int BORDER = 1; - - switch(myMsg.position) + if(myMsg.dirty) { - case MessagePosition::TopLeft: - myMsg.x = 5; - myMsg.y = 5; - break; + cerr << "--- draw message ---" << endl; - case MessagePosition::TopCenter: - myMsg.x = (imageRect().w() - dst.w()) >> 1; - myMsg.y = 5; - break; + // Draw the bounded box and text + const Common::Rect& dst = myMsg.surface->dstRect(); + const int fontWidth = font().getMaxCharWidth(), + fontHeight = font().getFontHeight(); + const int VBORDER = fontHeight / 4; + const int HBORDER = fontWidth * 1.25 / 2.0; + constexpr int BORDER = 1; - case MessagePosition::TopRight: - myMsg.x = imageRect().w() - dst.w() - 5; - myMsg.y = 5; - break; - - case MessagePosition::MiddleLeft: - myMsg.x = 5; - myMsg.y = (imageRect().h() - dst.h()) >> 1; - break; - - case MessagePosition::MiddleCenter: - myMsg.x = (imageRect().w() - dst.w()) >> 1; - myMsg.y = (imageRect().h() - dst.h()) >> 1; - break; - - case MessagePosition::MiddleRight: - myMsg.x = imageRect().w() - dst.w() - 5; - myMsg.y = (imageRect().h() - dst.h()) >> 1; - break; - - case MessagePosition::BottomLeft: - myMsg.x = 5; - myMsg.y = imageRect().h() - dst.h() - 5; - break; - - case MessagePosition::BottomCenter: - myMsg.x = (imageRect().w() - dst.w()) >> 1; - myMsg.y = imageRect().h() - dst.h() - 5; - break; - - case MessagePosition::BottomRight: - myMsg.x = imageRect().w() - dst.w() - 5; - myMsg.y = imageRect().h() - dst.h() - 5; - break; - } - - myMsg.surface->setDstPos(myMsg.x + imageRect().x(), myMsg.y + imageRect().y()); - myMsg.surface->fillRect(0, 0, myMsg.w, myMsg.h, kColor); - myMsg.surface->fillRect(BORDER, BORDER, myMsg.w - BORDER * 2, myMsg.h - BORDER * 2, kBtnColor); - myMsg.surface->drawString(font(), myMsg.text, HBORDER, VBORDER, - myMsg.w, myMsg.color); - - if(myMsg.showGauge) - { - constexpr int NUM_TICKMARKS = 4; - // limit gauge bar width if texts are too long - const int swidth = std::min(fontWidth * GAUGEBAR_WIDTH, - fontWidth * (MESSAGE_WIDTH - 2) - - font().getStringWidth(myMsg.text) - - font().getStringWidth(myMsg.valueText)); - const int bwidth = swidth * myMsg.value / 100.F; - const int bheight = fontHeight >> 1; - const int x = HBORDER + font().getStringWidth(myMsg.text) + fontWidth; - // align bar with bottom of text - const int y = VBORDER + font().desc().ascent - bheight; - - // draw gauge bar - myMsg.surface->fillRect(x - BORDER, y, swidth + BORDER * 2, bheight, kSliderBGColor); - myMsg.surface->fillRect(x, y + BORDER, bwidth, bheight - BORDER * 2, kSliderColor); - // draw tickmark in the middle of the bar - for(int i = 1; i < NUM_TICKMARKS; ++i) + switch(myMsg.position) { - ColorId color; - int xt = x + swidth * i / NUM_TICKMARKS; - if(bwidth < xt - x) - color = kCheckColor; // kSliderColor; - else - color = kSliderBGColor; - myMsg.surface->vLine(xt, y + bheight / 2, y + bheight - 1, color); + case MessagePosition::TopLeft: + myMsg.x = 5; + myMsg.y = 5; + break; + + case MessagePosition::TopCenter: + myMsg.x = (imageRect().w() - dst.w()) >> 1; + myMsg.y = 5; + break; + + case MessagePosition::TopRight: + myMsg.x = imageRect().w() - dst.w() - 5; + myMsg.y = 5; + break; + + case MessagePosition::MiddleLeft: + myMsg.x = 5; + myMsg.y = (imageRect().h() - dst.h()) >> 1; + break; + + case MessagePosition::MiddleCenter: + myMsg.x = (imageRect().w() - dst.w()) >> 1; + myMsg.y = (imageRect().h() - dst.h()) >> 1; + break; + + case MessagePosition::MiddleRight: + myMsg.x = imageRect().w() - dst.w() - 5; + myMsg.y = (imageRect().h() - dst.h()) >> 1; + break; + + case MessagePosition::BottomLeft: + myMsg.x = 5; + myMsg.y = imageRect().h() - dst.h() - 5; + break; + + case MessagePosition::BottomCenter: + myMsg.x = (imageRect().w() - dst.w()) >> 1; + myMsg.y = imageRect().h() - dst.h() - 5; + break; + + case MessagePosition::BottomRight: + myMsg.x = imageRect().w() - dst.w() - 5; + myMsg.y = imageRect().h() - dst.h() - 5; + break; } - // draw value text - myMsg.surface->drawString(font(), myMsg.valueText, - x + swidth + fontWidth, VBORDER, + + myMsg.surface->setDstPos(myMsg.x + imageRect().x(), myMsg.y + imageRect().y()); + myMsg.surface->fillRect(0, 0, myMsg.w, myMsg.h, kColor); + myMsg.surface->fillRect(BORDER, BORDER, myMsg.w - BORDER * 2, myMsg.h - BORDER * 2, kBtnColor); + myMsg.surface->drawString(font(), myMsg.text, HBORDER, VBORDER, myMsg.w, myMsg.color); + + if(myMsg.showGauge) + { + constexpr int NUM_TICKMARKS = 4; + // limit gauge bar width if texts are too long + const int swidth = std::min(fontWidth * GAUGEBAR_WIDTH, + fontWidth * (MESSAGE_WIDTH - 2) + - font().getStringWidth(myMsg.text) + - font().getStringWidth(myMsg.valueText)); + const int bwidth = swidth * myMsg.value / 100.F; + const int bheight = fontHeight >> 1; + const int x = HBORDER + font().getStringWidth(myMsg.text) + fontWidth; + // align bar with bottom of text + const int y = VBORDER + font().desc().ascent - bheight; + + // draw gauge bar + myMsg.surface->fillRect(x - BORDER, y, swidth + BORDER * 2, bheight, kSliderBGColor); + myMsg.surface->fillRect(x, y + BORDER, bwidth, bheight - BORDER * 2, kSliderColor); + // draw tickmark in the middle of the bar + for(int i = 1; i < NUM_TICKMARKS; ++i) + { + ColorId color; + int xt = x + swidth * i / NUM_TICKMARKS; + if(bwidth < xt - x) + color = kCheckColor; // kSliderColor; + else + color = kSliderBGColor; + myMsg.surface->vLine(xt, y + bheight / 2, y + bheight - 1, color); + } + // draw value text + myMsg.surface->drawString(font(), myMsg.valueText, + x + swidth + fontWidth, VBORDER, + myMsg.w, myMsg.color); + } + myMsg.dirty = false; + myMsg.surface->render(); + return true; } - myMsg.surface->render(); + myMsg.counter--; + myMsg.surface->render(); #endif - return true; + return false; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -898,7 +890,6 @@ void FrameBuffer::setUIPalette() void FrameBuffer::stateChanged(EventHandlerState state) { // Make sure any onscreen messages are removed - myMsg.enabled = false; myMsg.counter = 0; update(true); // force full update @@ -1010,7 +1001,7 @@ void FrameBuffer::toggleFullscreen(bool toggle) msg << "disabled ("; msg << "Zoom " << myActiveVidMode.zoom * 100 << "%)"; - showMessage(msg.str()); + showTextMessage(msg.str()); } break; } @@ -1043,7 +1034,7 @@ void FrameBuffer::toggleAdaptRefresh(bool toggle) msg << (isAdaptRefresh ? "enabled" : "disabled"); msg << " (" << myBackend->refreshRate() << " Hz)"; - showMessage(msg.str()); + showTextMessage(msg.str()); } } #endif @@ -1069,7 +1060,7 @@ void FrameBuffer::changeOverscan(int direction) val << (overscan > 0 ? "+" : "" ) << overscan << "%"; else val << "Off"; - myOSystem.frameBuffer().showMessage("Overscan", val.str(), overscan, 0, 10); + myOSystem.frameBuffer().showGaugeMessage("Overscan", val.str(), overscan, 0, 10); } } @@ -1106,9 +1097,9 @@ void FrameBuffer::switchVideoMode(int direction) if(applyVideoMode() == FBInitStatus::Success) { if(fullScreen()) - showMessage(myActiveVidMode.description); + showTextMessage(myActiveVidMode.description); else - showMessage("Zoom", myActiveVidMode.description, myActiveVidMode.zoom, + showGaugeMessage("Zoom", myActiveVidMode.description, myActiveVidMode.zoom, supportedTIAMinZoom(), myTIAMaxZoom); } } @@ -1248,9 +1239,9 @@ void FrameBuffer::toggleGrabMouse() myGrabMouse = !myGrabMouse; setCursorState(); myOSystem.settings().setValue("grabmouse", myGrabMouse); - myOSystem.frameBuffer().showMessage(oldState != myGrabMouse ? myGrabMouse - ? "Grab mouse enabled" : "Grab mouse disabled" - : "Grab mouse not allowed while cursor shown"); + myOSystem.frameBuffer().showTextMessage(oldState != myGrabMouse ? myGrabMouse + ? "Grab mouse enabled" : "Grab mouse disabled" + : "Grab mouse not allowed while cursor shown"); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/emucore/FrameBuffer.hxx b/src/emucore/FrameBuffer.hxx index c084c9cf6..8cda8d25c 100644 --- a/src/emucore/FrameBuffer.hxx +++ b/src/emucore/FrameBuffer.hxx @@ -92,15 +92,15 @@ class FrameBuffer void updateInEmulationMode(float framesPerSecond); /** - Shows a message onscreen. + Shows a text message onscreen. @param message The message to be shown @param position Onscreen position for the message @param force Force showing this message, even if messages are disabled */ - void showMessage(const string& message, - MessagePosition position = MessagePosition::BottomCenter, - bool force = false); + void showTextMessage(const string& message, + MessagePosition position = MessagePosition::BottomCenter, + bool force = false); /** Shows a message with a gauge bar onscreen. @@ -110,8 +110,8 @@ class FrameBuffer @param minValue The minimal value of the gauge bar @param maxValue The maximal value of the gauge bar */ - void showMessage(const string& message, const string& valueText, - float value, float minValue = 0.F, float maxValue = 100.F); + void showGaugeMessage(const string& message, const string& valueText, + float value, float minValue = 0.F, float maxValue = 100.F); bool messageShown() const; @@ -375,6 +375,18 @@ class FrameBuffer */ void resetSurfaces(); + #ifdef GUI_SUPPORT + /** + Helps to create a basic message onscreen. + + @param message The message to be shown + @param position Onscreen position for the message + @param force Force showing this message, even if messages are disabled + */ + void createMessage(const string& message, MessagePosition position, + bool force = false); + #endif + /** Draw pending messages. @@ -478,6 +490,7 @@ class FrameBuffer ColorId color{kNone}; shared_ptr surface; bool enabled{false}; + bool dirty{false}; bool showGauge{false}; float value{0.0F}; string valueText; diff --git a/src/emucore/OSystem.cxx b/src/emucore/OSystem.cxx index 611439e45..e659c1e5c 100644 --- a/src/emucore/OSystem.cxx +++ b/src/emucore/OSystem.cxx @@ -475,9 +475,9 @@ string OSystem::createConsole(const FilesystemNode& rom, const string& md5sum, { const string& id = myConsole->cartridge().multiCartID(); if(id == "") - myFrameBuffer->showMessage("New console created"); + myFrameBuffer->showTextMessage("New console created"); else - myFrameBuffer->showMessage("Multicart " + + myFrameBuffer->showTextMessage("Multicart " + myConsole->cartridge().detectedType() + ", loading ROM" + id); } buf << "Game console created:" << endl @@ -506,7 +506,7 @@ string OSystem::createConsole(const FilesystemNode& rom, const string& md5sum, msg << myConsole->leftController().name() << "/" << myConsole->rightController().name() << " - " << myConsole->cartridge().detectedType() << " - " << myConsole->getFormatString(); - myFrameBuffer->showMessage(msg.str()); + myFrameBuffer->showTextMessage(msg.str()); } } diff --git a/src/emucore/QuadTari.cxx b/src/emucore/QuadTari.cxx index 64c0cf027..f3fb00cdf 100644 --- a/src/emucore/QuadTari.cxx +++ b/src/emucore/QuadTari.cxx @@ -71,7 +71,7 @@ unique_ptr QuadTari::addController(const Controller::Type type, bool Controller::onMessageCallback callback = [&os = myOSystem](const string& msg) { bool devSettings = os.settings().getBool("dev.settings"); if(os.settings().getBool(devSettings ? "dev.eepromaccess" : "plr.eepromaccess")) - os.frameBuffer().showMessage(msg); + os.frameBuffer().showTextMessage(msg); }; switch(type) diff --git a/src/emucore/TIASurface.cxx b/src/emucore/TIASurface.cxx index 3858817a7..ec9dfe020 100644 --- a/src/emucore/TIASurface.cxx +++ b/src/emucore/TIASurface.cxx @@ -184,7 +184,7 @@ void TIASurface::setNTSC(NTSCFilter::Preset preset, bool show) } myOSystem.settings().setValue("tv.filter", int(preset)); - if(show) myFB.showMessage(buf.str()); + if(show) myFB.showTextMessage(buf.str()); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -221,7 +221,7 @@ void TIASurface::setNTSCAdjustable(int direction) setNTSC(NTSCFilter::Preset::CUSTOM); ntsc().selectAdjustable(direction, text, valueText, value); - myOSystem.frameBuffer().showMessage(text, valueText, value); + myOSystem.frameBuffer().showGaugeMessage(text, valueText, value); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -232,7 +232,7 @@ void TIASurface::changeNTSCAdjustable(int adjustable, int direction) setNTSC(NTSCFilter::Preset::CUSTOM); ntsc().changeAdjustable(adjustable, direction, text, valueText, newValue); - myOSystem.frameBuffer().showMessage(text, valueText, newValue); + myOSystem.frameBuffer().showGaugeMessage(text, valueText, newValue); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -243,7 +243,7 @@ void TIASurface::changeCurrentNTSCAdjustable(int direction) setNTSC(NTSCFilter::Preset::CUSTOM); ntsc().changeCurrentAdjustable(direction, text, valueText, newValue); - myOSystem.frameBuffer().showMessage(text, valueText, newValue); + myOSystem.frameBuffer().showGaugeMessage(text, valueText, newValue); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -259,7 +259,7 @@ void TIASurface::setScanlineIntensity(int direction) buf << intensity << "%"; else buf << "Off"; - myFB.showMessage("Scanline intensity", buf.str(), intensity); + myFB.showGaugeMessage("Scanline intensity", buf.str(), intensity); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 9649e447c..d4f4c165b 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -127,8 +127,8 @@ int DialogContainer::addDialog(Dialog* d) const uInt32 scale = myOSystem.frameBuffer().hidpiScaleFactor(); if(uInt32(d->getWidth() * scale) > r.w() || uInt32(d->getHeight() * scale) > r.h()) - myOSystem.frameBuffer().showMessage( - "Unable to show dialog box; FIX THE CODE"); + myOSystem.frameBuffer().showTextMessage( + "Unable to show dialog box; FIX THE CODE", MessagePosition::BottomCenter, true); else { // "darken" current top dialog diff --git a/src/gui/GameInfoDialog.cxx b/src/gui/GameInfoDialog.cxx index 5a3c4b1b5..57ad900d2 100644 --- a/src/gui/GameInfoDialog.cxx +++ b/src/gui/GameInfoDialog.cxx @@ -855,12 +855,12 @@ void GameInfoDialog::saveCurrentPropertiesToDisk() propfile /= myGameFile.getNameWithExt(".pro"); propfile.write(out); - instance().frameBuffer().showMessage("Properties saved to " + - propfile.getShortPath()); + instance().frameBuffer().showTextMessage("Properties saved to " + + propfile.getShortPath()); } catch(...) { - instance().frameBuffer().showMessage("Error saving properties"); + instance().frameBuffer().showTextMessage("Error saving properties"); } } diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 3f6ddcec8..8018bcb44 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -662,7 +662,7 @@ void LauncherDialog::loadRom() instance().settings().setValue("romdir", currentNode().getParent().getShortPath()); } else - instance().frameBuffer().showMessage(result, MessagePosition::MiddleCenter, true); + instance().frameBuffer().showTextMessage(result, MessagePosition::MiddleCenter, true); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/LoggerDialog.cxx b/src/gui/LoggerDialog.cxx index bf53d80a0..3f94e093e 100644 --- a/src/gui/LoggerDialog.cxx +++ b/src/gui/LoggerDialog.cxx @@ -123,12 +123,12 @@ void LoggerDialog::saveLogFile() { stringstream out; out << Logger::instance().logMessages(); - instance().frameBuffer().showMessage("Saving log file to " + node.getShortPath()); + instance().frameBuffer().showTextMessage("Saving log file to " + node.getShortPath()); node.write(out); } catch(...) { - instance().frameBuffer().showMessage("Error saving log file to " + node.getShortPath()); + instance().frameBuffer().showTextMessage("Error saving log file to " + node.getShortPath()); } } diff --git a/src/gui/TimeMachineDialog.cxx b/src/gui/TimeMachineDialog.cxx index e822504a9..1e983eb92 100644 --- a/src/gui/TimeMachineDialog.cxx +++ b/src/gui/TimeMachineDialog.cxx @@ -440,11 +440,11 @@ void TimeMachineDialog::handleCommand(CommandSender* sender, int cmd, break; case kSaveAll: - instance().frameBuffer().showMessage(instance().state().rewindManager().saveAllStates()); + instance().frameBuffer().showTextMessage(instance().state().rewindManager().saveAllStates()); break; case kLoadAll: - instance().frameBuffer().showMessage(instance().state().rewindManager().loadAllStates()); + instance().frameBuffer().showTextMessage(instance().state().rewindManager().loadAllStates()); initBar(); break; From e5f1e47f5df549de81301bd63b1fddc0a658f190 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 12 Nov 2020 11:50:26 +0100 Subject: [PATCH 014/107] further minimized UI redraws when message is displayed --- src/emucore/FrameBuffer.cxx | 53 ++++++++++++++++++------------------- src/gui/DialogContainer.cxx | 19 +++++++++++-- src/gui/DialogContainer.hxx | 5 ++++ 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index 66747b2a3..3af48cd15 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -322,10 +322,11 @@ void FrameBuffer::update(bool force) // - at the bottom of ::update(), to actually draw them (this must come // last, since they are always drawn on top of everything else). - // Forced full rendering is required when messages are being disabled - force |= (myMsg.enabled && myMsg.counter == 0); + // Forced rendering without drawing is required when messages are being disabled + // Only relevant for LAUNCHER, PAUSE and DEBUGGER modes + bool rerender = force || (myMsg.enabled && myMsg.counter == 0); - bool redraw = false; + bool redraw = force; switch(myOSystem.eventHandler().state()) { case EventHandlerState::NONE: @@ -341,7 +342,7 @@ void FrameBuffer::update(bool force) myPausedCount = uInt32(7 * myOSystem.frameRate()); showTextMessage("Paused", MessagePosition::MiddleCenter); } - if(force) + if(rerender) myTIASurface->render(); break; // EventHandlerState::PAUSE @@ -350,8 +351,8 @@ void FrameBuffer::update(bool force) #ifdef GUI_SUPPORT case EventHandlerState::OPTIONSMENU: { - redraw = myOSystem.menu().needsRedraw(); - if(force || redraw) + redraw |= myOSystem.menu().needsRedraw(); + if(redraw) { clear(); myTIASurface->render(); @@ -362,8 +363,8 @@ void FrameBuffer::update(bool force) case EventHandlerState::CMDMENU: { - redraw = myOSystem.commandMenu().needsRedraw(); - if(force || redraw) + redraw |= myOSystem.commandMenu().needsRedraw(); + if(redraw) { clear(); myTIASurface->render(); @@ -374,8 +375,8 @@ void FrameBuffer::update(bool force) case EventHandlerState::MESSAGEMENU: { - redraw = myOSystem.messageMenu().needsRedraw(); - if(force || redraw) + redraw |= myOSystem.messageMenu().needsRedraw(); + if(redraw) { clear(); myTIASurface->render(); @@ -386,8 +387,8 @@ void FrameBuffer::update(bool force) case EventHandlerState::TIMEMACHINE: { - redraw = myOSystem.timeMachine().needsRedraw(); - if(force || redraw) + redraw |= myOSystem.timeMachine().needsRedraw(); + if(redraw) { clear(); myTIASurface->render(); @@ -416,8 +417,8 @@ void FrameBuffer::update(bool force) r.rewindStates(1); } - force = force || success; - if(force) + redraw |= success; + if(redraw) myTIASurface->render(); // Stop playback mode at the end of the state buffer @@ -432,11 +433,11 @@ void FrameBuffer::update(bool force) case EventHandlerState::LAUNCHER: { - redraw = myOSystem.launcher().needsRedraw(); - if(force || redraw) - { + redraw |= myOSystem.launcher().needsRedraw(); + if(redraw) myOSystem.launcher().draw(force); - } + else if(rerender) + myOSystem.launcher().render(); break; // EventHandlerState::LAUNCHER } #endif @@ -444,12 +445,11 @@ void FrameBuffer::update(bool force) #ifdef DEBUGGER_SUPPORT case EventHandlerState::DEBUGGER: { - redraw = myOSystem.debugger().needsRedraw(); - if(force || redraw) - { - + redraw |= myOSystem.debugger().needsRedraw(); + if(redraw) myOSystem.debugger().draw(force); - } + else if(rerender) + myOSystem.debugger().render(); break; // EventHandlerState::DEBUGGER } #endif @@ -466,7 +466,7 @@ void FrameBuffer::update(bool force) redraw |= drawMessage(); // Push buffers to screen only when necessary - if(force || redraw) + if(redraw || rerender) myBackend->renderToScreen(); } @@ -518,7 +518,6 @@ void FrameBuffer::createMessage(const string& message, MessagePosition position, myMsg.enabled = true; myMsg.dirty = true; - myMsg.surface->setSrcSize(myMsg.w, myMsg.h); myMsg.surface->setDstSize(myMsg.w * hidpiScaleFactor(), myMsg.h * hidpiScaleFactor()); } @@ -666,7 +665,7 @@ void FrameBuffer::enableMessages(bool enable) // Erase old messages on the screen myMsg.counter = 0; - update(true); // Force update immediately + update(); // update immediately } } @@ -892,7 +891,7 @@ void FrameBuffer::stateChanged(EventHandlerState state) // Make sure any onscreen messages are removed myMsg.counter = 0; - update(true); // force full update + update(); // update immediately } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index d4f4c165b..77e163e83 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -91,21 +91,36 @@ void DialogContainer::updateTime(uInt64 time) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DialogContainer::draw(bool full) { - cerr << "draw " << full << " " << typeid(*this).name() << endl; if(myDialogStack.empty()) return; + cerr << "draw " << full << " " << typeid(*this).name() << endl; + // Make the top dialog dirty if a full redraw is requested if(full) myDialogStack.top()->setDirty(); - // Render all dirty dialogs + // Draw and render all dirty dialogs myDialogStack.applyAll([&](Dialog*& d) { if(d->needsRedraw()) d->redraw(); }); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void DialogContainer::render() +{ + if(myDialogStack.empty()) + return; + + cerr << "render " << typeid(*this).name() << endl; + + // Render all dirty dialogs + myDialogStack.applyAll([&](Dialog*& d) { + d->render(); + }); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool DialogContainer::needsRedraw() const { diff --git a/src/gui/DialogContainer.hxx b/src/gui/DialogContainer.hxx index 1f6cce8f6..70950606d 100644 --- a/src/gui/DialogContainer.hxx +++ b/src/gui/DialogContainer.hxx @@ -124,6 +124,11 @@ class DialogContainer */ void draw(bool full = false); + /** + Render the stack of menus. + */ + void render(); + /** Answers whether a full redraw is required. */ From 9819118b5934cab775c4da0414bd99c1386597e4 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 12 Nov 2020 14:04:29 +0100 Subject: [PATCH 015/107] replaced shaded UI redraws with shading surface --- src/gui/Dialog.cxx | 26 +++++++++++++++++++++++--- src/gui/Dialog.hxx | 1 + src/gui/DialogContainer.cxx | 13 ++++++++----- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 9cb5accb2..6f8867cfb 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -227,8 +227,6 @@ void Dialog::redraw() center(); drawDialog(); render(); - -// return true; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -242,6 +240,28 @@ void Dialog::render() surface->render(); }); } + if(parent().myDialogStack.top() != this) + { + if(_shadeSurface == nullptr) + { + uInt32 data = 0xff000000; + + _shadeSurface = instance().frameBuffer().allocateSurface( + 1, 1, ScalingInterpolation::sharp, &data); + + FBSurface::Attributes& attr = _shadeSurface->attributes(); + + attr.blending = true; + attr.blendalpha = 25; // darken background dialogs by 25% + _shadeSurface->applyAttributes(); + } + + const Common::Rect& rect = _surface->dstRect(); + _shadeSurface->setDstPos(rect.x(), rect.y()); + _shadeSurface->setDstSize(rect.w(), rect.h()); + + _shadeSurface->render(); + } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -408,7 +428,7 @@ void Dialog::drawDialog() //cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; // Dialog is still on top if e.g a ContextMenu is opened - _onTop = parent().myDialogStack.top() == this + _onTop = true/*parent().myDialogStack.top() == this*/ || (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this && !parent().myDialogStack.top()->hasTitle()); diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index b9a924b55..d5405ef70 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -230,6 +230,7 @@ class Dialog : public GuiObject WidgetArray _buttonGroup; shared_ptr _surface; + shared_ptr _shadeSurface; int _tabID{0}; uInt32 _max_w{0}; // maximum wanted width diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 77e163e83..98deae834 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -146,9 +146,10 @@ int DialogContainer::addDialog(Dialog* d) "Unable to show dialog box; FIX THE CODE", MessagePosition::BottomCenter, true); else { - // "darken" current top dialog - if(!myDialogStack.empty()) - myDialogStack.top()->setDirty(); + //// "shade" current top dialog + //if(!myDialogStack.empty()) + // myDialogStack.top()->setDirty(); + d->setDirty(); myDialogStack.push(d); } @@ -164,14 +165,16 @@ void DialogContainer::removeDialog() if(!myDialogStack.empty()) { - // this "undarkens" the top dialog - myDialogStack.top()->setDirty(); + //// this "unshades" the top dialog + //myDialogStack.top()->setDirty(); // Rerender all dialogs (TODO: top dialog is rendered twice) myDialogStack.applyAll([&](Dialog*& d){ //d->setDirty(); d->render(); }); + // TODO: the screen is not updated until an event happens + // FrameBuffer::myBackend->renderToScreen() doesn't help } } } From 907fc4edf3d743213b0f101ba023a2903de5771d Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 12 Nov 2020 14:37:50 +0100 Subject: [PATCH 016/107] minimized ContextMenu redraws fixed shading caused by ContextMenu --- src/gui/ContextMenu.cxx | 8 ++++++-- src/gui/Dialog.cxx | 11 ++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/gui/ContextMenu.cxx b/src/gui/ContextMenu.cxx index 9cf626657..a53f1c4ae 100644 --- a/src/gui/ContextMenu.cxx +++ b/src/gui/ContextMenu.cxx @@ -346,8 +346,12 @@ int ContextMenu::findItem(int x, int y) const void ContextMenu::drawCurrentSelection(int item) { // Change selection - _selectedOffset = item; - setDirty(); + if(_selectedOffset != item) + { + _selectedOffset = item; + cerr << "ContextMenu" << endl; + setDirty(); + } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 6f8867cfb..fe0d06d37 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -240,7 +240,15 @@ void Dialog::render() surface->render(); }); } - if(parent().myDialogStack.top() != this) + + //cerr << "is ContextMenu " + // << (typeid(*parent().myDialogStack.top()) == typeid(ContextMenu)) << endl; + + // Dialog is still on top if e.g a ContextMenu is opened + if(!(parent().myDialogStack.top() == this) + && !((parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this + //&& !(typeid(*parent().myDialogStack.top()) == typeid(ContextMenu))) + && !parent().myDialogStack.top()->hasTitle()))) { if(_shadeSurface == nullptr) { @@ -432,6 +440,7 @@ void Dialog::drawDialog() || (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this && !parent().myDialogStack.top()->hasTitle()); + cerr << "on top " << isOnTop() << endl; if(clearsBackground()) { // cerr << "Dialog::drawDialog(): w = " << _w << ", h = " << _h << " @ " << &s << endl << endl; From 42817a61171194c480ba7216e6a18cbbbf8a501d Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 12 Nov 2020 15:41:40 +0100 Subject: [PATCH 017/107] Allow first click detection when Stella lost focus. --- src/common/EventHandlerSDL2.cxx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/EventHandlerSDL2.cxx b/src/common/EventHandlerSDL2.cxx index e5900cfba..39b3469f5 100644 --- a/src/common/EventHandlerSDL2.cxx +++ b/src/common/EventHandlerSDL2.cxx @@ -45,6 +45,8 @@ EventHandlerSDL2::EventHandlerSDL2(OSystem& osystem) } Logger::debug("EventHandlerSDL2::EventHandlerSDL2 SDL_INIT_JOYSTICK"); #endif + + SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -91,6 +93,7 @@ void EventHandlerSDL2::pollEvent() while(SDL_PollEvent(&myEvent)) { + cerr << myEvent.type << endl; switch(myEvent.type) { // keyboard events @@ -198,6 +201,7 @@ void EventHandlerSDL2::pollEvent() } case SDL_WINDOWEVENT: + cerr << myEvent.window.event << endl; switch(myEvent.window.event) { case SDL_WINDOWEVENT_SHOWN: From f0d6b672eaf1a4c63c7c32dba09ac1420f0c3d0c Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 12 Nov 2020 15:48:00 +0100 Subject: [PATCH 018/107] removed debug code --- src/common/EventHandlerSDL2.cxx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/EventHandlerSDL2.cxx b/src/common/EventHandlerSDL2.cxx index 39b3469f5..86d4dad89 100644 --- a/src/common/EventHandlerSDL2.cxx +++ b/src/common/EventHandlerSDL2.cxx @@ -93,7 +93,6 @@ void EventHandlerSDL2::pollEvent() while(SDL_PollEvent(&myEvent)) { - cerr << myEvent.type << endl; switch(myEvent.type) { // keyboard events @@ -201,7 +200,6 @@ void EventHandlerSDL2::pollEvent() } case SDL_WINDOWEVENT: - cerr << myEvent.window.event << endl; switch(myEvent.window.event) { case SDL_WINDOWEVENT_SHOWN: From c390b40a6d6cb2670dc4425969ac0e2418efd1a4 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 12 Nov 2020 18:14:26 +0100 Subject: [PATCH 019/107] refactored UI ticks --- src/debugger/gui/DebuggerDialog.hxx | 2 +- src/debugger/gui/RomListSettings.cxx | 2 +- src/debugger/gui/RomListSettings.hxx | 4 +-- src/emucore/FrameBuffer.cxx | 6 ++++ src/gui/ContextMenu.cxx | 2 +- src/gui/ContextMenu.hxx | 4 +-- src/gui/Dialog.cxx | 35 +++++++++++----------- src/gui/Dialog.hxx | 7 ++--- src/gui/DialogContainer.cxx | 7 +++++ src/gui/DialogContainer.hxx | 5 ++++ src/gui/EditableWidget.cxx | 6 ++-- src/gui/EditableWidget.hxx | 2 +- src/gui/GuiObject.hxx | 10 +++++-- src/gui/InputTextDialog.cxx | 4 +-- src/gui/InputTextDialog.hxx | 2 +- src/gui/LauncherDialog.hxx | 2 +- src/gui/TimeMachineDialog.cxx | 2 +- src/gui/TimeMachineDialog.hxx | 4 +-- src/gui/Widget.cxx | 44 ++++++++++------------------ src/gui/Widget.hxx | 3 +- 20 files changed, 80 insertions(+), 73 deletions(-) diff --git a/src/debugger/gui/DebuggerDialog.hxx b/src/debugger/gui/DebuggerDialog.hxx index 0affa5160..51633c825 100644 --- a/src/debugger/gui/DebuggerDialog.hxx +++ b/src/debugger/gui/DebuggerDialog.hxx @@ -76,7 +76,7 @@ class DebuggerDialog : public Dialog void saveConfig() override; private: - void center() override { positionAt(0); } + void setPosition() override { positionAt(0); } void loadConfig() override; void handleKeyDown(StellaKey key, StellaMod mod, bool repeated) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override; diff --git a/src/debugger/gui/RomListSettings.cxx b/src/debugger/gui/RomListSettings.cxx index 9c3baf3d2..93b555631 100644 --- a/src/debugger/gui/RomListSettings.cxx +++ b/src/debugger/gui/RomListSettings.cxx @@ -100,7 +100,7 @@ void RomListSettings::show(uInt32 x, uInt32 y, const Common::Rect& bossRect, int } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void RomListSettings::center() +void RomListSettings::setPosition() { // First set position according to original coordinates surface().setDstPos(_xorig, _yorig); diff --git a/src/debugger/gui/RomListSettings.hxx b/src/debugger/gui/RomListSettings.hxx index 0845dd8ab..a589b0334 100644 --- a/src/debugger/gui/RomListSettings.hxx +++ b/src/debugger/gui/RomListSettings.hxx @@ -38,8 +38,8 @@ class RomListSettings : public Dialog, public CommandSender ('data' will be the currently selected line number in RomListWidget) */ void show(uInt32 x, uInt32 y, const Common::Rect& bossRect, int data = -1); - /** This dialog uses its own positioning, so we override Dialog::center() */ - void center() override; + /** This dialog uses its own positioning, so we override Dialog::setPosition() */ + void setPosition() override; private: uInt32 _xorig{0}, _yorig{0}; diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index 3af48cd15..a151084d1 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -351,6 +351,7 @@ void FrameBuffer::update(bool force) #ifdef GUI_SUPPORT case EventHandlerState::OPTIONSMENU: { + myOSystem.menu().tick(); redraw |= myOSystem.menu().needsRedraw(); if(redraw) { @@ -363,6 +364,7 @@ void FrameBuffer::update(bool force) case EventHandlerState::CMDMENU: { + myOSystem.commandMenu().tick(); redraw |= myOSystem.commandMenu().needsRedraw(); if(redraw) { @@ -375,6 +377,7 @@ void FrameBuffer::update(bool force) case EventHandlerState::MESSAGEMENU: { + myOSystem.messageMenu().tick(); redraw |= myOSystem.messageMenu().needsRedraw(); if(redraw) { @@ -387,6 +390,7 @@ void FrameBuffer::update(bool force) case EventHandlerState::TIMEMACHINE: { + myOSystem.timeMachine().tick(); redraw |= myOSystem.timeMachine().needsRedraw(); if(redraw) { @@ -433,6 +437,7 @@ void FrameBuffer::update(bool force) case EventHandlerState::LAUNCHER: { + myOSystem.launcher().tick(); redraw |= myOSystem.launcher().needsRedraw(); if(redraw) myOSystem.launcher().draw(force); @@ -445,6 +450,7 @@ void FrameBuffer::update(bool force) #ifdef DEBUGGER_SUPPORT case EventHandlerState::DEBUGGER: { + myOSystem.debugger().tick(); redraw |= myOSystem.debugger().needsRedraw(); if(redraw) myOSystem.debugger().draw(force); diff --git a/src/gui/ContextMenu.cxx b/src/gui/ContextMenu.cxx index a53f1c4ae..ae1ee09be 100644 --- a/src/gui/ContextMenu.cxx +++ b/src/gui/ContextMenu.cxx @@ -77,7 +77,7 @@ void ContextMenu::show(uInt32 x, uInt32 y, const Common::Rect& bossRect, int ite } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ContextMenu::center() +void ContextMenu::setPosition() { // First set position according to original coordinates surface().setDstPos(_xorig, _yorig); diff --git a/src/gui/ContextMenu.hxx b/src/gui/ContextMenu.hxx index 423ef3074..f1736e517 100644 --- a/src/gui/ContextMenu.hxx +++ b/src/gui/ContextMenu.hxx @@ -71,8 +71,8 @@ class ContextMenu : public Dialog, public CommandSender const string& getSelectedName() const; const Variant& getSelectedTag() const; - /** This dialog uses its own positioning, so we override Dialog::center() */ - void center() override; + /** This dialog uses its own positioning, so we override Dialog::setPosition() */ + void setPosition() override; /** The following methods are used when we want to select *and* send a command for the new selection. They are only to be used diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index fe0d06d37..e58920e89 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -91,7 +91,7 @@ void Dialog::open() const uInt32 scale = instance().frameBuffer().hidpiScaleFactor(); _surface->setDstSize(_w * scale, _h * scale); - center(); + setPosition(); if(_myTabList.size()) // (Re)-build the focus list to use for all widgets of all tabs @@ -143,32 +143,20 @@ void Dialog::setTitle(const string& title) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void Dialog::center() +void Dialog::setPosition() { positionAt(instance().settings().getInt("dialogpos")); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void Dialog::setDirty() -{ - _dirty = true; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool Dialog::isDirty() -{ - return _dirty; -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool Dialog::isChainDirty() const { bool dirty = false; - // Check if widget or any subwidgets are dirty + // Recursively check if dialog or any chick dialogs or widgets are dirty Widget* w = _firstWidget; - while(!dirty && w) + while(w && !dirty) { dirty |= w->needsRedraw(); w = w->_next; @@ -177,6 +165,19 @@ bool Dialog::isChainDirty() const return dirty; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Dialog::tick() +{ + // Recursively tick dialog and all child dialogs and widgets + Widget* w = _firstWidget; + + while(w) + { + w->tick(); + w = w->_next; + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Dialog::positionAt(uInt32 pos) { @@ -224,7 +225,7 @@ void Dialog::redraw() return;// false; // Draw this dialog - center(); + setPosition(); drawDialog(); render(); } diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index d5405ef70..c594d93cd 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -56,16 +56,13 @@ class Dialog : public GuiObject bool isVisible() const override { return _visible; } bool isOnTop() const { return _onTop; } - virtual void center(); + virtual void setPosition(); virtual void drawDialog(); virtual void loadConfig() { } virtual void saveConfig() { } virtual void setDefaults() { } - // A dialog being dirty indicates that its underlying surface needs to be - // redrawn and then re-rendered; this is taken care of in ::render() - void setDirty() override; - bool isDirty() override; // TODO: remove + void tick() override; bool isChainDirty() const override; void redraw(); void render(); diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 98deae834..1210e5d7c 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -107,6 +107,13 @@ void DialogContainer::draw(bool full) }); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void DialogContainer::tick() +{ + if(!myDialogStack.empty()) + myDialogStack.top()->tick(); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DialogContainer::render() { diff --git a/src/gui/DialogContainer.hxx b/src/gui/DialogContainer.hxx index 70950606d..134d82e55 100644 --- a/src/gui/DialogContainer.hxx +++ b/src/gui/DialogContainer.hxx @@ -119,6 +119,11 @@ class DialogContainer */ void handleJoyHatEvent(int stick, int hat, JoyHatDir hdir, int button); + /** + Tick the dialog and all its widgets. + */ + void tick(); + /** Draw the stack of menus (full indicates to redraw all items). */ diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index 4032daae3..4c3928052 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -62,8 +62,9 @@ void EditableWidget::setText(const string& str, bool) setDirty(); } + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool EditableWidget::isDirty() +void EditableWidget::tick() { if(_hasFocus && _editable && isVisible() && _boss->isVisible()) { @@ -75,8 +76,7 @@ bool EditableWidget::isDirty() _dirty = true; } } - - return _dirty; + Widget::tick(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/EditableWidget.hxx b/src/gui/EditableWidget.hxx index e4dfbc2f7..345a4c618 100644 --- a/src/gui/EditableWidget.hxx +++ b/src/gui/EditableWidget.hxx @@ -67,7 +67,7 @@ class EditableWidget : public Widget, public CommandSender protected: void receivedFocusWidget() override; void lostFocusWidget() override; - bool isDirty() override; + void tick() override; virtual void startEditMode() { setFlags(Widget::FLAG_WANTS_RAWDATA); } virtual void endEditMode() { clearFlags(Widget::FLAG_WANTS_RAWDATA); } diff --git a/src/gui/GuiObject.hxx b/src/gui/GuiObject.hxx index 811f1569b..581d498aa 100644 --- a/src/gui/GuiObject.hxx +++ b/src/gui/GuiObject.hxx @@ -91,11 +91,15 @@ class GuiObject : public CommandReceiver virtual void setHeight(int h) { _h = h; } virtual bool isVisible() const = 0; - virtual void setDirty() = 0; + virtual void setDirty() { _dirty = true; } virtual void clearDirty() { _dirty = false; } - virtual bool isDirty() { return _dirty; } + + virtual void tick() = 0; + virtual bool isDirty() const { return _dirty; } virtual bool isChainDirty() const = 0; - virtual bool needsRedraw() { return isDirty() || isChainDirty(); }; + // The GUI indicates if its underlying surface needs to be redrawn + // and then re-rendered + virtual bool needsRedraw() { return isDirty() || isChainDirty(); } void setFlags(uInt32 flags) { diff --git a/src/gui/InputTextDialog.cxx b/src/gui/InputTextDialog.cxx index 12e7a66d4..54b656a67 100644 --- a/src/gui/InputTextDialog.cxx +++ b/src/gui/InputTextDialog.cxx @@ -130,7 +130,7 @@ void InputTextDialog::show(uInt32 x, uInt32 y, const Common::Rect& bossRect) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void InputTextDialog::center() +void InputTextDialog::setPosition() { if(!myEnableCenter) { @@ -144,7 +144,7 @@ void InputTextDialog::center() surface().setDstPos(myXOrig, myYOrig); } else - Dialog::center(); + Dialog::setPosition(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/InputTextDialog.hxx b/src/gui/InputTextDialog.hxx index e89e00651..a06ca4446 100644 --- a/src/gui/InputTextDialog.hxx +++ b/src/gui/InputTextDialog.hxx @@ -58,7 +58,7 @@ class InputTextDialog : public Dialog, public CommandSender void handleCommand(CommandSender* sender, int cmd, int data, int id) override; /** This dialog uses its own positioning, so we override Dialog::center() */ - void center() override; + void setPosition() override; private: vector myInput; diff --git a/src/gui/LauncherDialog.hxx b/src/gui/LauncherDialog.hxx index 0dd9b898d..cc265928b 100644 --- a/src/gui/LauncherDialog.hxx +++ b/src/gui/LauncherDialog.hxx @@ -101,7 +101,7 @@ class LauncherDialog : public Dialog static constexpr int MIN_ROMINFO_ROWS = 7; // full lines static constexpr int MIN_ROMINFO_LINES = 4; // extra lines - void center() override { positionAt(0); } + void setPosition() override { positionAt(0); } void handleKeyDown(StellaKey key, StellaMod mod, bool repeated) override; void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override; diff --git a/src/gui/TimeMachineDialog.cxx b/src/gui/TimeMachineDialog.cxx index 1e983eb92..8e550d2a9 100644 --- a/src/gui/TimeMachineDialog.cxx +++ b/src/gui/TimeMachineDialog.cxx @@ -297,7 +297,7 @@ TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TimeMachineDialog::center() +void TimeMachineDialog::setPosition() { // Place on the bottom of the screen, centered horizontally const Common::Size& screen = instance().frameBuffer().screenSize(); diff --git a/src/gui/TimeMachineDialog.hxx b/src/gui/TimeMachineDialog.hxx index a4a489b79..1d15d6b15 100644 --- a/src/gui/TimeMachineDialog.hxx +++ b/src/gui/TimeMachineDialog.hxx @@ -44,8 +44,8 @@ class TimeMachineDialog : public Dialog /** initialize timeline bar */ void initBar(); - /** This dialog uses its own positioning, so we override Dialog::center() */ - void center() override; + /** This dialog uses its own positioning, so we override Dialog::setPosition() */ + void setPosition() override; /** convert cycles into time */ string getTimeString(uInt64 cycles); diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 69c94b3ed..951a60282 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -51,48 +51,36 @@ Widget::~Widget() _focusList.clear(); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void Widget::setDirty() -{ - // A widget being dirty indicates that its parent dialog is dirty - // So we inform the parent about it - //_boss->dialog().setDirty(); - //cerr << "set dirty " << typeid(*this).name() << endl; - - _dirty = true; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool Widget::isDirty() -{ - //string name = typeid(*this).name(); - //if(_dirty && name == "class TabWidget") - // cerr << "is dirty " << typeid(*this).name() << endl; - - return _dirty; -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool Widget::isChainDirty() const { - string name = typeid(*this).name(); - if(_dirty && name == "class TabWidget") - cerr << "is chain dirty " << typeid(*this).name() << endl; - bool dirty = false; - // Check if widget or any subwidgets are dirty + // Recursively check if widget or any child dialogs or widgets are dirty Widget* w = _firstWidget; - while(!dirty && w) + while(w && !dirty) { - dirty |= w->isDirty(); + dirty |= w->needsRedraw(); w = w->_next; } return dirty; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::tick() +{ + // Recursively tick widget and all child dialogs and widgets + Widget* w = _firstWidget; + + while(w) + { + w->tick(); + w = w->_next; + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Widget::draw() { diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 3e70523c0..6a505e8df 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -69,8 +69,7 @@ class Widget : public GuiObject virtual bool handleJoyHat(int stick, int hat, JoyHatDir hdir, int button = JOY_CTRL_NONE) { return false; } virtual bool handleEvent(Event::Type event) { return false; } - void setDirty() override; - bool isDirty() override; // TODO: remove + void tick() override; bool isChainDirty() const override; void draw() override; void receivedFocus(); From 1a5a0b5286cfd67a403a9ccbd16d2e7fa85f77fd Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 12 Nov 2020 19:46:28 +0100 Subject: [PATCH 020/107] fixed AboutDialog widget overlapping --- src/gui/AboutDialog.cxx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/gui/AboutDialog.cxx b/src/gui/AboutDialog.cxx index db088d923..426adbeeb 100644 --- a/src/gui/AboutDialog.cxx +++ b/src/gui/AboutDialog.cxx @@ -67,11 +67,12 @@ AboutDialog::AboutDialog(OSystem& osystem, DialogContainer& parent, addCancelWidget(b); xpos = HBORDER; ypos = _th + VBORDER + (buttonHeight - fontHeight) / 2; - myTitle = new StaticTextWidget(this, font, xpos, ypos, _w - xpos * 2, fontHeight, - "", TextAlign::Center); + int bwidth = font.getStringWidth("What's New" + ELLIPSIS) + fontWidth * 2.5; + + myTitle = new StaticTextWidget(this, font, xpos + bwidth, ypos, _w - (xpos + bwidth) * 2, + fontHeight, "", TextAlign::Center); myTitle->setTextColor(kTextColorEm); - int bwidth = font.getStringWidth("What's New" + ELLIPSIS) + fontWidth * 2.5; myWhatsNewButton = new ButtonWidget(this, font, _w - HBORDER - bwidth, ypos - (buttonHeight - fontHeight) / 2, bwidth, buttonHeight, "What's New" + ELLIPSIS, kWhatsNew); From ebd88377296ea101afa6bc7a6f6d8d1b0258efe5 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Thu, 12 Nov 2020 18:04:56 -0330 Subject: [PATCH 021/107] Fix compile warning. --- src/emucore/FBSurface.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emucore/FBSurface.hxx b/src/emucore/FBSurface.hxx index d72ffdd64..ea851e6e6 100644 --- a/src/emucore/FBSurface.hxx +++ b/src/emucore/FBSurface.hxx @@ -323,7 +323,7 @@ class FBSurface This method should be called to reset the surface to empty pixels / colour black. */ - virtual void invalidate() {}; + virtual void invalidate() {} /** This method should be called to reset a surface area to empty From c787e940f2fa57df69665b66d8d6542322cb4dbf Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 13 Nov 2020 08:58:19 +0100 Subject: [PATCH 022/107] fixed rendering, all dialogs are always re-rendered --- src/emucore/FrameBuffer.hxx | 7 +++++- src/gui/Dialog.cxx | 22 ++++++++-------- src/gui/Dialog.hxx | 5 ++++ src/gui/DialogContainer.cxx | 26 ++++++++----------- src/gui/ToolTip.cxx | 40 ++++++++++++++++++++++++++++++ src/gui/ToolTip.hxx | 39 +++++++++++++++++++++++++++++ src/gui/Widget.cxx | 21 +++++++++++----- src/gui/Widget.hxx | 4 +++ src/windows/Stella.vcxproj | 2 ++ src/windows/Stella.vcxproj.filters | 6 +++++ 10 files changed, 139 insertions(+), 33 deletions(-) create mode 100644 src/gui/ToolTip.cxx create mode 100644 src/gui/ToolTip.hxx diff --git a/src/emucore/FrameBuffer.hxx b/src/emucore/FrameBuffer.hxx index 8cda8d25c..9d6f3e930 100644 --- a/src/emucore/FrameBuffer.hxx +++ b/src/emucore/FrameBuffer.hxx @@ -88,9 +88,14 @@ class FrameBuffer /** There is a dedicated update method for emulation mode. - */ + */ void updateInEmulationMode(float framesPerSecond); + /** + Render backend to screen. + */ + void renderToScreen() { myBackend->renderToScreen(); } + /** Shows a text message onscreen. diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index e58920e89..b483d6e96 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -110,8 +110,6 @@ void Dialog::open() loadConfig(); // has to be done AFTER (re)building the focus list _visible = true; - - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -227,12 +225,14 @@ void Dialog::redraw() // Draw this dialog setPosition(); drawDialog(); - render(); + // full rendering is caused in dialog container } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Dialog::render() { + cerr << " render " << typeid(*this).name() << endl; + // Update dialog surface; also render any extra surfaces // Extra surfaces must be rendered afterwards, so they are drawn on top if(_surface->render()) @@ -242,15 +242,17 @@ void Dialog::render() }); } - //cerr << "is ContextMenu " - // << (typeid(*parent().myDialogStack.top()) == typeid(ContextMenu)) << endl; + // Dialog is still on top if e.g a dialog without title is opened + // (e.g. ContextMenu) + bool onTop = parent().myDialogStack.top() == this + || (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this + && !parent().myDialogStack.top()->hasTitle()); + //&& typeid(*parent().myDialogStack.top()) == typeid(ContextMenu)) - // Dialog is still on top if e.g a ContextMenu is opened - if(!(parent().myDialogStack.top() == this) - && !((parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this - //&& !(typeid(*parent().myDialogStack.top()) == typeid(ContextMenu))) - && !parent().myDialogStack.top()->hasTitle()))) + if(!onTop) { + cerr << " shade " << typeid(*this).name() << endl; + if(_shadeSurface == nullptr) { uInt32 data = 0xff000000; diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index c594d93cd..4fc48b7fc 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -119,6 +119,10 @@ class Dialog : public GuiObject */ bool shouldResize(uInt32& w, uInt32& h) const; + //bool enableToolTip(); + //void showToolTip(int x, int y); + //void hideToolTip(); + protected: void draw() override { } void releaseFocus() override; @@ -197,6 +201,7 @@ class Dialog : public GuiObject string _title; int _th{0}; int _layer{0}; + int _toolTipTimer{0}; Common::FixedStack> mySurfaceStack; diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 1210e5d7c..50db35d3f 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -97,14 +97,16 @@ void DialogContainer::draw(bool full) cerr << "draw " << full << " " << typeid(*this).name() << endl; // Make the top dialog dirty if a full redraw is requested - if(full) - myDialogStack.top()->setDirty(); + //if(full) + // myDialogStack.top()->setDirty(); // Draw and render all dirty dialogs myDialogStack.applyAll([&](Dialog*& d) { if(d->needsRedraw()) d->redraw(); }); + // Always render all surfaces, bottom to top + render(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -120,9 +122,9 @@ void DialogContainer::render() if(myDialogStack.empty()) return; - cerr << "render " << typeid(*this).name() << endl; + cerr << "full re-render " << typeid(*this).name() << endl; - // Render all dirty dialogs + // Render all dialogs myDialogStack.applyAll([&](Dialog*& d) { d->render(); }); @@ -153,10 +155,6 @@ int DialogContainer::addDialog(Dialog* d) "Unable to show dialog box; FIX THE CODE", MessagePosition::BottomCenter, true); else { - //// "shade" current top dialog - //if(!myDialogStack.empty()) - // myDialogStack.top()->setDirty(); - d->setDirty(); myDialogStack.push(d); } @@ -168,20 +166,16 @@ void DialogContainer::removeDialog() { if(!myDialogStack.empty()) { + cerr << "remove dialog" << endl; myDialogStack.pop(); if(!myDialogStack.empty()) { - //// this "unshades" the top dialog - //myDialogStack.top()->setDirty(); - - // Rerender all dialogs (TODO: top dialog is rendered twice) + // Rerender all dialogs myDialogStack.applyAll([&](Dialog*& d){ - //d->setDirty(); - d->render(); + d->render(); }); - // TODO: the screen is not updated until an event happens - // FrameBuffer::myBackend->renderToScreen() doesn't help + myOSystem.frameBuffer().renderToScreen(); } } } diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx new file mode 100644 index 000000000..53cbea0c3 --- /dev/null +++ b/src/gui/ToolTip.cxx @@ -0,0 +1,40 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2020 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "OSystem.hxx" +#include "Font.hxx" +#include "Dialog.hxx" +#include "DialogContainer.hxx" + +#include "ToolTip.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +ToolTip::ToolTip(OSystem& instance, DialogContainer& parent, + const GUI::Font& font) + : Dialog(instance, parent, font) +{ + const int lineHeight = font.getLineHeight(), + fontWidth = font.getMaxCharWidth(), + fontHeight = font.getFontHeight(), + buttonWidth = font.getStringWidth("Previous") + fontWidth * 2.5, + buttonHeight = font.getLineHeight() * 1.25; + const int VBORDER = fontHeight / 2; + const int HBORDER = fontWidth * 1.25; + const int VGAP = fontHeight / 4; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx new file mode 100644 index 000000000..2066bd3df --- /dev/null +++ b/src/gui/ToolTip.hxx @@ -0,0 +1,39 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2020 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef TOOL_TIP_HXX +#define TOOL_TIP_HXX + +class OSystem; +class DialogContainer; + +/** + * Class for providing tooltip functionality + * + * @author Thomas Jentzsch + */ +class ToolTip : public Dialog +{ + public: + + public: + ToolTip(OSystem& instance, DialogContainer& parent, + const GUI::Font& font); + ~ToolTip() override = default; +}; + +#endif diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 951a60282..1cacda9d6 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -71,13 +71,22 @@ bool Widget::isChainDirty() const // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Widget::tick() { - // Recursively tick widget and all child dialogs and widgets - Widget* w = _firstWidget; - - while(w) + if(isEnabled()) { - w->tick(); - w = w->_next; + //if(_hasFocus && hasToolTip()) + //{ + // if(dialog().enableToolTip()) + // dialog().showToolTip(10, 10); + //} + + // Recursively tick widget and all child dialogs and widgets + Widget* w = _firstWidget; + + while(w) + { + w->tick(); + w = w->_next; + } } } diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 6a505e8df..13eefdbf7 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -99,6 +99,9 @@ class Widget : public GuiObject void setBGColorHi(ColorId color) { _bgcolorhi = color; setDirty(); } void setShadowColor(ColorId color) { _shadowcolor = color; setDirty(); } + void setToolTip(const string& text) { _toolTipText = text; } + bool hasToolTip() const { return !_toolTipText.empty(); } + virtual void loadConfig() { } protected: @@ -130,6 +133,7 @@ class Widget : public GuiObject ColorId _textcolorhi{kTextColorHi}; ColorId _textcolorlo{kBGColorLo}; ColorId _shadowcolor{kShadowColor}; + string _toolTipText; public: static Widget* findWidgetInChain(Widget* start, int x, int y); diff --git a/src/windows/Stella.vcxproj b/src/windows/Stella.vcxproj index 5f0bd9126..f9b5d3499 100644 --- a/src/windows/Stella.vcxproj +++ b/src/windows/Stella.vcxproj @@ -786,6 +786,7 @@ + @@ -1838,6 +1839,7 @@ + diff --git a/src/windows/Stella.vcxproj.filters b/src/windows/Stella.vcxproj.filters index 8dca2be2e..550419e9a 100644 --- a/src/windows/Stella.vcxproj.filters +++ b/src/windows/Stella.vcxproj.filters @@ -1032,6 +1032,9 @@ Source Files\gui + + Source Files\gui + @@ -2123,6 +2126,9 @@ Header Files\gui + + Header Files\gui + From 15576fe6b1ea6626d6405fa8b34462ae7e7be1d1 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 13 Nov 2020 09:36:57 +0100 Subject: [PATCH 023/107] fixed forced full redraws force full UI redraw when UI palette changes --- src/emucore/FrameBuffer.cxx | 2 ++ src/gui/Dialog.cxx | 9 ++++++--- src/gui/Dialog.hxx | 2 +- src/gui/DialogContainer.cxx | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index a151084d1..2622b8fe9 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -889,6 +889,8 @@ void FrameBuffer::setUIPalette() myFullPalette[j] = mapRGB(r, g, b); } FBSurface::setPalette(myFullPalette); + if(&myOSystem.eventHandler()) + update(true); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index b483d6e96..e1bf15f3e 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -217,10 +217,13 @@ void Dialog::positionAt(uInt32 pos) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void Dialog::redraw() +void Dialog::redraw(bool force) { - if(!isVisible() || !needsRedraw()) - return;// false; + if(!isVisible()) + return; + + if(force) + setDirty(); // Draw this dialog setPosition(); diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index 4fc48b7fc..abb22911a 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -64,7 +64,7 @@ class Dialog : public GuiObject void tick() override; bool isChainDirty() const override; - void redraw(); + void redraw(bool force = false); void render(); void addFocusWidget(Widget* w) override; diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 50db35d3f..7b001cb06 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -102,8 +102,8 @@ void DialogContainer::draw(bool full) // Draw and render all dirty dialogs myDialogStack.applyAll([&](Dialog*& d) { - if(d->needsRedraw()) - d->redraw(); + if(full || d->needsRedraw()) + d->redraw(full); }); // Always render all surfaces, bottom to top render(); From 7c962fbfe7c80d602b3524a5fe2bd7ed5f52b2c4 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 13 Nov 2020 10:03:03 +0100 Subject: [PATCH 024/107] avoid full update when window gets exposed (test) --- src/emucore/EventHandler.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emucore/EventHandler.cxx b/src/emucore/EventHandler.cxx index 9e7c9ea5f..cb372c7f9 100644 --- a/src/emucore/EventHandler.cxx +++ b/src/emucore/EventHandler.cxx @@ -323,7 +323,8 @@ void EventHandler::handleSystemEvent(SystemEvent e, int, int) { case SystemEvent::WINDOW_EXPOSED: case SystemEvent::WINDOW_RESIZED: - myOSystem.frameBuffer().update(true); // force full update + //myOSystem.frameBuffer().update(true); // force full update + myOSystem.frameBuffer().update(); break; #ifdef BSPF_UNIX case SystemEvent::WINDOW_FOCUS_GAINED: From 36f3810e405829d994e5ad1056f3be721d5045d9 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 13 Nov 2020 11:18:25 +0100 Subject: [PATCH 025/107] fixed missing render when a stacked dialog was closed in emulation --- src/emucore/FrameBuffer.cxx | 30 +++++++++++++++++++----------- src/emucore/FrameBuffer.hxx | 9 ++++++--- src/gui/DialogContainer.cxx | 10 ++-------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index 2622b8fe9..ffc827ba9 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -311,7 +311,7 @@ FBInitStatus FrameBuffer::createDisplay(const string& title, BufferType type, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void FrameBuffer::update(bool force) +void FrameBuffer::update(bool forceRedraw) { // Onscreen messages are a special case and require different handling than // other objects; they aren't UI dialogs in the normal sense nor are they @@ -322,11 +322,12 @@ void FrameBuffer::update(bool force) // - at the bottom of ::update(), to actually draw them (this must come // last, since they are always drawn on top of everything else). - // Forced rendering without drawing is required when messages are being disabled - // Only relevant for LAUNCHER, PAUSE and DEBUGGER modes - bool rerender = force || (myMsg.enabled && myMsg.counter == 0); + // Forced render without draw required if messages or dialogs were closed + // Note: For dialogs only relevant when two or more dialogs were stacked + bool rerender = forceRedraw || myPendingRender; + myPendingRender = false; - bool redraw = force; + bool redraw = forceRedraw; switch(myOSystem.eventHandler().state()) { case EventHandlerState::NONE: @@ -357,7 +358,13 @@ void FrameBuffer::update(bool force) { clear(); myTIASurface->render(); - myOSystem.menu().draw(force); + myOSystem.menu().draw(forceRedraw); + } + else if(rerender) + { + clear(); + myTIASurface->render(); + myOSystem.menu().render(); } break; // EventHandlerState::OPTIONSMENU } @@ -370,7 +377,7 @@ void FrameBuffer::update(bool force) { clear(); myTIASurface->render(); - myOSystem.commandMenu().draw(force); + myOSystem.commandMenu().draw(forceRedraw); } break; // EventHandlerState::CMDMENU } @@ -383,7 +390,7 @@ void FrameBuffer::update(bool force) { clear(); myTIASurface->render(); - myOSystem.messageMenu().draw(force); + myOSystem.messageMenu().draw(forceRedraw); } break; // EventHandlerState::MESSAGEMENU } @@ -396,7 +403,7 @@ void FrameBuffer::update(bool force) { clear(); myTIASurface->render(); - myOSystem.timeMachine().draw(force); + myOSystem.timeMachine().draw(forceRedraw); } break; // EventHandlerState::TIMEMACHINE } @@ -440,7 +447,7 @@ void FrameBuffer::update(bool force) myOSystem.launcher().tick(); redraw |= myOSystem.launcher().needsRedraw(); if(redraw) - myOSystem.launcher().draw(force); + myOSystem.launcher().draw(forceRedraw); else if(rerender) myOSystem.launcher().render(); break; // EventHandlerState::LAUNCHER @@ -453,7 +460,7 @@ void FrameBuffer::update(bool force) myOSystem.debugger().tick(); redraw |= myOSystem.debugger().needsRedraw(); if(redraw) - myOSystem.debugger().draw(force); + myOSystem.debugger().draw(forceRedraw); else if(rerender) myOSystem.debugger().render(); break; // EventHandlerState::DEBUGGER @@ -684,6 +691,7 @@ inline bool FrameBuffer::drawMessage() if(myMsg.counter == 0) { myMsg.enabled = false; + myPendingRender = true; return false; } diff --git a/src/emucore/FrameBuffer.hxx b/src/emucore/FrameBuffer.hxx index 9d6f3e930..78106f8c0 100644 --- a/src/emucore/FrameBuffer.hxx +++ b/src/emucore/FrameBuffer.hxx @@ -84,7 +84,7 @@ class FrameBuffer Updates the display, which depending on the current mode could mean drawing the TIA, any pending menus, etc. */ - void update(bool force = false); + void update(bool forceRedraw = false); /** There is a dedicated update method for emulation mode. @@ -92,9 +92,9 @@ class FrameBuffer void updateInEmulationMode(float framesPerSecond); /** - Render backend to screen. + Set pending rendering flag. */ - void renderToScreen() { myBackend->renderToScreen(); } + void setPendingRender() { myPendingRender = true; } /** Shows a text message onscreen. @@ -460,6 +460,9 @@ class FrameBuffer // Supported renderers VariantList myRenderers; + // Flag for pending render + bool myPendingRender{false}; + // The VideoModeHandler class takes responsibility for all video // mode functionality VideoModeHandler myVidModeHandler; diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 7b001cb06..c2011e7e0 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -169,14 +169,8 @@ void DialogContainer::removeDialog() cerr << "remove dialog" << endl; myDialogStack.pop(); - if(!myDialogStack.empty()) - { - // Rerender all dialogs - myDialogStack.applyAll([&](Dialog*& d){ - d->render(); - }); - myOSystem.frameBuffer().renderToScreen(); - } + // Inform the frame buffer that it has to render all surfaces + myOSystem.frameBuffer().setPendingRender(); } } From 9900564862698914980e6c6a23617ac4e160d467 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 13 Nov 2020 11:19:48 +0100 Subject: [PATCH 026/107] disabled palette display if without console --- src/gui/VideoAudioDialog.cxx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/gui/VideoAudioDialog.cxx b/src/gui/VideoAudioDialog.cxx index f68be6df8..ba141cbeb 100644 --- a/src/gui/VideoAudioDialog.cxx +++ b/src/gui/VideoAudioDialog.cxx @@ -1157,10 +1157,11 @@ void VideoAudioDialog::addPalette(int x, int y, int w, int h) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void VideoAudioDialog::colorPalette() { + constexpr int NUM_LUMA = 8; + constexpr int NUM_CHROMA = 16; + if(instance().hasConsole()) { - constexpr int NUM_LUMA = 8; - constexpr int NUM_CHROMA = 16; const int order[2][NUM_CHROMA] = { {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, @@ -1176,11 +1177,14 @@ void VideoAudioDialog::colorPalette() ss << Common::Base::HEX1 << std::uppercase << color; myColorLbl[idx]->setLabel(ss.str()); for(int lum = 0; lum < NUM_LUMA; ++lum) - { myColor[idx][lum]->setColor(color * NUM_CHROMA + lum * 2); // skip grayscale colors - } } } + else + // disable palette + for(int idx = 0; idx < NUM_CHROMA; ++idx) + for(int lum = 0; lum < NUM_LUMA; ++lum) + myColor[idx][lum]->setEnabled(false); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From bcbf0072eafbecc6c7e9a69beae9c2cfb0cb4c0a Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 13 Nov 2020 14:24:52 +0100 Subject: [PATCH 027/107] fixed initial focus display --- src/emucore/EventHandler.cxx | 1 + src/gui/Dialog.cxx | 22 +++++++++++----------- src/gui/Widget.cxx | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/emucore/EventHandler.cxx b/src/emucore/EventHandler.cxx index cb372c7f9..59c17c8a8 100644 --- a/src/emucore/EventHandler.cxx +++ b/src/emucore/EventHandler.cxx @@ -324,6 +324,7 @@ void EventHandler::handleSystemEvent(SystemEvent e, int, int) case SystemEvent::WINDOW_EXPOSED: case SystemEvent::WINDOW_RESIZED: //myOSystem.frameBuffer().update(true); // force full update + // TODO: test and maybe force a render update instead myOSystem.frameBuffer().update(); break; #ifdef BSPF_UNIX diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index e1bf15f3e..95379398a 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -476,17 +476,6 @@ void Dialog::drawDialog() clearDirty(); } - // Draw outlines for focused widgets - // Don't change focus, since this will trigger lost and received - // focus events - if(_focusedWidget) - { - _focusedWidget = Widget::setFocusForChain(this, getFocusList(), - _focusedWidget, 0, false); - // if(_focusedWidget) - // _focusedWidget->draw(); // make sure the highlight color is drawn initially - } - Widget* w = _firstWidget; // Draw all children @@ -498,6 +487,17 @@ void Dialog::drawDialog() w->draw(); w = w->_next; } + + // Draw outlines for focused widgets + // Don't change focus, since this will trigger lost and received + // focus events + if(_focusedWidget) + { + _focusedWidget = Widget::setFocusForChain(this, getFocusList(), + _focusedWidget, 0, false); + //if(_focusedWidget) + // _focusedWidget->draw(); // make sure the highlight color is drawn initially + } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 1cacda9d6..93331cea1 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -269,7 +269,7 @@ Widget* Widget::setFocusForChain(GuiObject* boss, WidgetArray& arr, s.frameRect(x, y, w, h, onTop ? kDlgColor : kBGColorLo); - tmp->setDirty(); + //tmp->setDirty(); } } @@ -325,7 +325,7 @@ Widget* Widget::setFocusForChain(GuiObject* boss, WidgetArray& arr, if (onTop) s.frameRect(x, y, w, h, kWidFrameColor, FrameStyle::Dashed); - tmp->setDirty(); + //tmp->setDirty(); return tmp; } From d656598fa34184afca2b761a44b5553f9640da96 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Fri, 13 Nov 2020 10:05:11 -0330 Subject: [PATCH 028/107] Update Xcode for class addition. Comment out code that causes a crash on Mac. --- src/emucore/FrameBuffer.cxx | 4 ++-- src/macos/stella.xcodeproj/project.pbxproj | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index ffc827ba9..de1156200 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -897,8 +897,8 @@ void FrameBuffer::setUIPalette() myFullPalette[j] = mapRGB(r, g, b); } FBSurface::setPalette(myFullPalette); - if(&myOSystem.eventHandler()) - update(true); +// if(&myOSystem.eventHandler()) +// update(true); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/macos/stella.xcodeproj/project.pbxproj b/src/macos/stella.xcodeproj/project.pbxproj index 4452ddce2..a23a0b173 100644 --- a/src/macos/stella.xcodeproj/project.pbxproj +++ b/src/macos/stella.xcodeproj/project.pbxproj @@ -531,6 +531,8 @@ DCBDDE9B1D6A5F0E009DF1E9 /* Cart3EPlusWidget.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCBDDE991D6A5F0E009DF1E9 /* Cart3EPlusWidget.hxx */; }; DCBDDE9E1D6A5F2F009DF1E9 /* Cart3EPlus.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCBDDE9C1D6A5F2F009DF1E9 /* Cart3EPlus.cxx */; }; DCBDDE9F1D6A5F2F009DF1E9 /* Cart3EPlus.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCBDDE9D1D6A5F2F009DF1E9 /* Cart3EPlus.hxx */; }; + DCC2FDF5255EB82500FA5E81 /* ToolTip.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC2FDF3255EB82500FA5E81 /* ToolTip.hxx */; }; + DCC2FDF6255EB82500FA5E81 /* ToolTip.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCC2FDF4255EB82500FA5E81 /* ToolTip.cxx */; }; DCC527D110B9DA19005E1287 /* Device.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC527C910B9DA19005E1287 /* Device.hxx */; }; DCC527D210B9DA19005E1287 /* M6502.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCC527CA10B9DA19005E1287 /* M6502.cxx */; }; DCC527D310B9DA19005E1287 /* M6502.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC527CB10B9DA19005E1287 /* M6502.hxx */; }; @@ -1298,6 +1300,8 @@ DCBDDE991D6A5F0E009DF1E9 /* Cart3EPlusWidget.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Cart3EPlusWidget.hxx; sourceTree = ""; }; DCBDDE9C1D6A5F2F009DF1E9 /* Cart3EPlus.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Cart3EPlus.cxx; sourceTree = ""; }; DCBDDE9D1D6A5F2F009DF1E9 /* Cart3EPlus.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Cart3EPlus.hxx; sourceTree = ""; }; + DCC2FDF3255EB82500FA5E81 /* ToolTip.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ToolTip.hxx; sourceTree = ""; }; + DCC2FDF4255EB82500FA5E81 /* ToolTip.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ToolTip.cxx; sourceTree = ""; }; DCC527C910B9DA19005E1287 /* Device.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Device.hxx; sourceTree = ""; }; DCC527CA10B9DA19005E1287 /* M6502.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = M6502.cxx; sourceTree = ""; }; DCC527CB10B9DA19005E1287 /* M6502.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = M6502.hxx; sourceTree = ""; }; @@ -2167,6 +2171,8 @@ DCA82C6E1FEB4E780059340F /* TimeMachine.hxx */, DCA82C6F1FEB4E780059340F /* TimeMachineDialog.cxx */, DCA82C701FEB4E780059340F /* TimeMachineDialog.hxx */, + DCC2FDF4255EB82500FA5E81 /* ToolTip.cxx */, + DCC2FDF3255EB82500FA5E81 /* ToolTip.hxx */, DC8078E60B4BD697005E9305 /* UIDialog.cxx */, DC8078E70B4BD697005E9305 /* UIDialog.hxx */, DCBA539825557E2800087DD7 /* UndoHandler.cxx */, @@ -2651,6 +2657,7 @@ DC5D1AA7102C6FC900E59AC1 /* Stack.hxx in Headers */, DCF7B0DE10A762FC007A2870 /* CartF0.hxx in Headers */, DCF7B0E010A762FC007A2870 /* CartFA.hxx in Headers */, + DCC2FDF5255EB82500FA5E81 /* ToolTip.hxx in Headers */, DCC527D110B9DA19005E1287 /* Device.hxx in Headers */, DC6F394E21B897F300897AD8 /* ThreadDebugging.hxx in Headers */, DCC527D310B9DA19005E1287 /* M6502.hxx in Headers */, @@ -3205,6 +3212,7 @@ DCAACB0E188D636F00A4D282 /* Cart4KSCWidget.cxx in Sources */, DCB60AD02543100900A5C1D2 /* FBBackendSDL2.cxx in Sources */, DCAACB10188D636F00A4D282 /* CartBFSCWidget.cxx in Sources */, + DCC2FDF6255EB82500FA5E81 /* ToolTip.cxx in Sources */, DCAACB12188D636F00A4D282 /* CartBFWidget.cxx in Sources */, DCAACB14188D636F00A4D282 /* CartDFSCWidget.cxx in Sources */, DCAACB16188D636F00A4D282 /* CartDFWidget.cxx in Sources */, From 2505201b4b3125d0251356ae09036d21f498c0d7 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 13 Nov 2020 16:00:19 +0100 Subject: [PATCH 029/107] fixed UI palette update crash fixed garbage when switching state in fullscreen modes --- src/emucore/EventHandler.cxx | 1 + src/emucore/FrameBuffer.cxx | 20 +++++++++++++------- src/emucore/FrameBuffer.hxx | 5 +++++ src/gui/DialogContainer.cxx | 3 +++ src/gui/StellaSettingsDialog.cxx | 1 + src/gui/UIDialog.cxx | 1 + 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/emucore/EventHandler.cxx b/src/emucore/EventHandler.cxx index 59c17c8a8..3164a92a4 100644 --- a/src/emucore/EventHandler.cxx +++ b/src/emucore/EventHandler.cxx @@ -2320,6 +2320,7 @@ bool EventHandler::enterDebugMode() } myOverlay->reStack(); myOSystem.sound().mute(true); + #else myOSystem.frameBuffer().showTextMessage("Debugger support not included", MessagePosition::BottomCenter, true); diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index de1156200..c51cdbbce 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -270,7 +270,7 @@ FBInitStatus FrameBuffer::createDisplay(const string& title, BufferType type, #ifdef GUI_SUPPORT // Erase any messages from a previous run - myMsg.counter = 0; + myMsg.enabled = false; // Create surfaces for TIA statistics and general messages const GUI::Font& f = hidpiEnabled() ? infoFont() : font(); @@ -677,11 +677,19 @@ void FrameBuffer::enableMessages(bool enable) myStatsMsg.enabled = false; // Erase old messages on the screen - myMsg.counter = 0; + hideMessage(); + update(); // update immediately } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void FrameBuffer::hideMessage() +{ + myPendingRender = myMsg.enabled; + myMsg.enabled = false; +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - inline bool FrameBuffer::drawMessage() { @@ -690,8 +698,7 @@ inline bool FrameBuffer::drawMessage() // or show again this frame if(myMsg.counter == 0) { - myMsg.enabled = false; - myPendingRender = true; + hideMessage(); return false; } @@ -897,15 +904,13 @@ void FrameBuffer::setUIPalette() myFullPalette[j] = mapRGB(r, g, b); } FBSurface::setPalette(myFullPalette); -// if(&myOSystem.eventHandler()) -// update(true); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FrameBuffer::stateChanged(EventHandlerState state) { // Make sure any onscreen messages are removed - myMsg.counter = 0; + hideMessage(); update(); // update immediately } @@ -1169,6 +1174,7 @@ FBInitStatus FrameBuffer::applyVideoMode() resetSurfaces(); setCursorState(); + myPendingRender = true; } else Logger::error("ERROR: Couldn't initialize video subsystem"); diff --git a/src/emucore/FrameBuffer.hxx b/src/emucore/FrameBuffer.hxx index 78106f8c0..e5e6bd1b6 100644 --- a/src/emucore/FrameBuffer.hxx +++ b/src/emucore/FrameBuffer.hxx @@ -399,6 +399,11 @@ class FrameBuffer */ bool drawMessage(); + /** + Hide pending messages. + */ + void hideMessage(); + /** Draws the frame stats overlay. */ diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index c2011e7e0..1c75273b4 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -181,6 +181,9 @@ void DialogContainer::reStack() while(!myDialogStack.empty()) myDialogStack.top()->close(); + // Make sure that all surfaces are cleared + myOSystem.frameBuffer().clear(); + baseDialog()->open(); // Reset all continuous events diff --git a/src/gui/StellaSettingsDialog.cxx b/src/gui/StellaSettingsDialog.cxx index fc3d3e4d9..dbd2ac09c 100644 --- a/src/gui/StellaSettingsDialog.cxx +++ b/src/gui/StellaSettingsDialog.cxx @@ -268,6 +268,7 @@ void StellaSettingsDialog::saveConfig() settings.setValue("uipalette", myThemePopup->getSelectedTag().toString()); instance().frameBuffer().setUIPalette(); + instance().frameBuffer().update(true); // Dialog position settings.setValue("dialogpos", myPositionPopup->getSelectedTag().toString()); diff --git a/src/gui/UIDialog.cxx b/src/gui/UIDialog.cxx index 1b64d73f2..1a75ee440 100644 --- a/src/gui/UIDialog.cxx +++ b/src/gui/UIDialog.cxx @@ -441,6 +441,7 @@ void UIDialog::saveConfig() settings.setValue("uipalette", myPalettePopup->getSelectedTag().toString()); instance().frameBuffer().setUIPalette(); + instance().frameBuffer().update(true); // Dialog font settings.setValue("dialogfont", From db55dc44208a87dbc8806ea2b347dba7495f3e12 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 13 Nov 2020 16:12:33 +0100 Subject: [PATCH 030/107] improved fullscreen message in debugger mode --- src/emucore/FrameBuffer.cxx | 38 ++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index c51cdbbce..823b864c1 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -927,10 +927,10 @@ string FrameBuffer::getDisplayKey() case BufferType::Emulator: return "display"; - #ifdef DEBUGGER_SUPPORT + #ifdef DEBUGGER_SUPPORT case BufferType::Debugger: return "dbg.display"; - #endif + #endif default: return ""; @@ -949,10 +949,10 @@ string FrameBuffer::getPositionKey() case BufferType::Emulator: return "windowedpos"; - #ifdef DEBUGGER_SUPPORT + #ifdef DEBUGGER_SUPPORT case BufferType::Debugger: return "dbg.pos"; - #endif + #endif default: return ""; @@ -963,10 +963,10 @@ string FrameBuffer::getPositionKey() void FrameBuffer::saveCurrentWindowPosition() { myOSystem.settings().setValue( - getDisplayKey(), myBackend->getCurrentDisplayIndex()); + getDisplayKey(), myBackend->getCurrentDisplayIndex()); if(myBackend->isCurrentWindowPositioned()) myOSystem.settings().setValue( - getPositionKey(), myBackend->getCurrentWindowPos()); + getPositionKey(), myBackend->getCurrentWindowPos()); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1001,7 +1001,9 @@ void FrameBuffer::setFullscreen(bool enable) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FrameBuffer::toggleFullscreen(bool toggle) { - switch(myOSystem.eventHandler().state()) + EventHandlerState state = myOSystem.eventHandler().state(); + + switch(state) { case EventHandlerState::LAUNCHER: case EventHandlerState::EMULATION: @@ -1011,16 +1013,26 @@ void FrameBuffer::toggleFullscreen(bool toggle) const bool isFullscreen = toggle ? !fullScreen() : fullScreen(); setFullscreen(isFullscreen); - if(myBufferType != BufferType::Launcher) + if(state != EventHandlerState::LAUNCHER) { ostringstream msg; msg << "Fullscreen "; - if(isFullscreen) - msg << "enabled (" << myBackend->refreshRate() << " Hz, "; - else - msg << "disabled ("; - msg << "Zoom " << myActiveVidMode.zoom * 100 << "%)"; + if(state != EventHandlerState::DEBUGGER) + { + if(isFullscreen) + msg << "enabled (" << myBackend->refreshRate() << " Hz, "; + else + msg << "disabled ("; + msg << "Zoom " << myActiveVidMode.zoom * 100 << "%)"; + } + else + { + if(isFullscreen) + msg << "enabled"; + else + msg << "disabled"; + } showTextMessage(msg.str()); } break; From ed13b214026675e087a44f8c805461642429d5e3 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 13 Nov 2020 19:53:19 +0100 Subject: [PATCH 031/107] added a full render when event WINDOW_EXPOSED and WINDOW_RESIZED are handled stopped screen from changing frames when 'Pause' is displayed --- src/emucore/EventHandler.cxx | 5 ++--- src/emucore/FrameBuffer.cxx | 13 ++++++++----- src/emucore/FrameBuffer.hxx | 8 +++++++- src/gui/StellaSettingsDialog.cxx | 2 +- src/gui/UIDialog.cxx | 2 +- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/emucore/EventHandler.cxx b/src/emucore/EventHandler.cxx index 3164a92a4..7d241101e 100644 --- a/src/emucore/EventHandler.cxx +++ b/src/emucore/EventHandler.cxx @@ -323,9 +323,8 @@ void EventHandler::handleSystemEvent(SystemEvent e, int, int) { case SystemEvent::WINDOW_EXPOSED: case SystemEvent::WINDOW_RESIZED: - //myOSystem.frameBuffer().update(true); // force full update - // TODO: test and maybe force a render update instead - myOSystem.frameBuffer().update(); + // Force full render update + myOSystem.frameBuffer().update(FrameBuffer::UpdateMode::RERENDER); break; #ifdef BSPF_UNIX case SystemEvent::WINDOW_FOCUS_GAINED: diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index 823b864c1..fce9cdae4 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -311,7 +311,7 @@ FBInitStatus FrameBuffer::createDisplay(const string& title, BufferType type, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void FrameBuffer::update(bool forceRedraw) +void FrameBuffer::update(UpdateMode mode) { // Onscreen messages are a special case and require different handling than // other objects; they aren't UI dialogs in the normal sense nor are they @@ -322,12 +322,15 @@ void FrameBuffer::update(bool forceRedraw) // - at the bottom of ::update(), to actually draw them (this must come // last, since they are always drawn on top of everything else). + bool forceRedraw = mode & UpdateMode::REDRAW; + bool redraw = forceRedraw; + // Forced render without draw required if messages or dialogs were closed // Note: For dialogs only relevant when two or more dialogs were stacked - bool rerender = forceRedraw || myPendingRender; + bool rerender = (mode & (UpdateMode::REDRAW | UpdateMode::RERENDER)) + || myPendingRender; myPendingRender = false; - bool redraw = forceRedraw; switch(myOSystem.eventHandler().state()) { case EventHandlerState::NONE: @@ -342,10 +345,10 @@ void FrameBuffer::update(bool forceRedraw) { myPausedCount = uInt32(7 * myOSystem.frameRate()); showTextMessage("Paused", MessagePosition::MiddleCenter); + myTIASurface->render(); } if(rerender) myTIASurface->render(); - break; // EventHandlerState::PAUSE } @@ -857,7 +860,7 @@ void FrameBuffer::resetSurfaces() freeSurfaces(); reloadSurfaces(); - update(true); // force full update + update(UpdateMode::REDRAW); // force full update } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/emucore/FrameBuffer.hxx b/src/emucore/FrameBuffer.hxx index e5e6bd1b6..0971873f2 100644 --- a/src/emucore/FrameBuffer.hxx +++ b/src/emucore/FrameBuffer.hxx @@ -55,6 +55,12 @@ class FrameBuffer // Zoom level step interval static constexpr float ZOOM_STEPS = 0.25; + enum UpdateMode { + NONE = 0, + REDRAW = 1, + RERENDER = 2 + }; + public: FrameBuffer(OSystem& osystem); ~FrameBuffer(); @@ -84,7 +90,7 @@ class FrameBuffer Updates the display, which depending on the current mode could mean drawing the TIA, any pending menus, etc. */ - void update(bool forceRedraw = false); + void update(UpdateMode mode = UpdateMode::NONE); /** There is a dedicated update method for emulation mode. diff --git a/src/gui/StellaSettingsDialog.cxx b/src/gui/StellaSettingsDialog.cxx index dbd2ac09c..06ff03b3e 100644 --- a/src/gui/StellaSettingsDialog.cxx +++ b/src/gui/StellaSettingsDialog.cxx @@ -268,7 +268,7 @@ void StellaSettingsDialog::saveConfig() settings.setValue("uipalette", myThemePopup->getSelectedTag().toString()); instance().frameBuffer().setUIPalette(); - instance().frameBuffer().update(true); + instance().frameBuffer().update(FrameBuffer::UpdateMode::REDRAW); // Dialog position settings.setValue("dialogpos", myPositionPopup->getSelectedTag().toString()); diff --git a/src/gui/UIDialog.cxx b/src/gui/UIDialog.cxx index 1a75ee440..12f710bc0 100644 --- a/src/gui/UIDialog.cxx +++ b/src/gui/UIDialog.cxx @@ -441,7 +441,7 @@ void UIDialog::saveConfig() settings.setValue("uipalette", myPalettePopup->getSelectedTag().toString()); instance().frameBuffer().setUIPalette(); - instance().frameBuffer().update(true); + instance().frameBuffer().update(FrameBuffer::UpdateMode::REDRAW); // Dialog font settings.setValue("dialogfont", From f52e834455955d7c906dd180cc8afa7edd9d4513 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 14 Nov 2020 09:43:41 +0100 Subject: [PATCH 032/107] fixed breakpoints setting in RomListWidget improved drawing of breakpoints in RomListWidget made RomListWidget redraw regularly only if in edit mode --- src/debugger/gui/RomListWidget.cxx | 32 +++++++++++++++--------------- src/debugger/gui/RomListWidget.hxx | 1 - src/debugger/gui/RomWidget.cxx | 4 ---- src/gui/Dialog.cxx | 10 +++++++--- src/gui/Dialog.hxx | 1 + src/gui/EditableWidget.cxx | 2 +- src/gui/EditableWidget.hxx | 1 + src/gui/GuiObject.hxx | 11 ++++++---- src/gui/Widget.cxx | 7 +++++++ src/gui/Widget.hxx | 1 + 10 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 20fc53052..3504b2b37 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -39,6 +39,8 @@ RomListWidget::RomListWidget(GuiObject* boss, const GUI::Font& lfont, _textcolor = kTextColor; _textcolorhi = kTextColor; + _editMode = false; + _cols = w / _fontWidth; _rows = h / _lineHeight; @@ -480,20 +482,18 @@ void RomListWidget::drawWidget(bool hilite) codeDisasmW = actualWidth; xpos = _x + CheckboxWidget::boxSize(_font) + 10; ypos = _y + 2; - for (i = 0, pos = _currentPos; i < _rows && pos < len; i++, pos++, ypos += _lineHeight) + for(i = 0, pos = _currentPos; i < _rows && pos < len; i++, pos++, ypos += _lineHeight) { ColorId bytesColor = textColor; - // Draw checkboxes for correct lines (takes scrolling into account) + // Mark checkboxes dirty for correct lines (takes scrolling into account) myCheckList[i]->setState(instance().debugger(). checkBreakPoint(dlist[pos].address, instance().debugger().cartDebug().getBank(dlist[pos].address))); - myCheckList[i]->setDirty(); - myCheckList[i]->draw(); // Draw highlighted item in a frame - if (_highlightedItem == pos) + if(_highlightedItem == pos) s.frameRect(_x + l.x() - 3, ypos - 1, _w - l.x(), _lineHeight, onTop ? kWidColorHi : kBGColorLo); // Draw the selected item inverted, on a highlighted background. @@ -510,31 +510,31 @@ void RomListWidget::drawWidget(bool hilite) // Draw labels s.drawString(_font, dlist[pos].label, xpos, ypos, _labelWidth, - dlist[pos].hllabel ? textColor : kColor); + dlist[pos].hllabel ? textColor : kColor); // Bytes are only editable if they represent code, graphics, or accessible data // Otherwise, the disassembly should get all remaining space - if(dlist[pos].type & (Device::CODE|Device::GFX|Device::PGFX| - Device::COL|Device::PCOL|Device::BCOL|Device::DATA)) + if(dlist[pos].type & (Device::CODE | Device::GFX | Device::PGFX | + Device::COL | Device::PCOL | Device::BCOL | Device::DATA)) { if(dlist[pos].type == Device::CODE) { // Draw mnemonic s.drawString(_font, dlist[pos].disasm.substr(0, 7), xpos + _labelWidth, ypos, - 7 * _fontWidth, textColor); + 7 * _fontWidth, textColor); // Draw operand - if (dlist[pos].disasm.length() > 8) + if(dlist[pos].disasm.length() > 8) s.drawString(_font, dlist[pos].disasm.substr(8), xpos + _labelWidth + 7 * _fontWidth, ypos, - codeDisasmW - 7 * _fontWidth, textColor); + codeDisasmW - 7 * _fontWidth, textColor); // Draw cycle count s.drawString(_font, dlist[pos].ccount, xpos + _labelWidth + codeDisasmW, ypos, - cycleCountW, textColor); + cycleCountW, textColor); } else { // Draw disassembly only s.drawString(_font, dlist[pos].disasm, xpos + _labelWidth, ypos, - noCodeDisasmW - 4, kTextColor); + noCodeDisasmW - 4, kTextColor); } // Draw separator @@ -542,11 +542,11 @@ void RomListWidget::drawWidget(bool hilite) // Draw bytes { - if (_selectedItem == pos && _editMode) + if(_selectedItem == pos && _editMode) { adjustOffset(); s.drawString(_font, editString(), _x + r.x(), ypos, r.w(), textColor, - TextAlign::Left, -_editScrollOffset, false); + TextAlign::Left, -_editScrollOffset, false); drawCaretSelection(); } @@ -560,7 +560,7 @@ void RomListWidget::drawWidget(bool hilite) { // Draw disassembly, giving it all remaining horizontal space s.drawString(_font, dlist[pos].disasm, xpos + _labelWidth, ypos, - noTypeDisasmW, textColor); + noTypeDisasmW, textColor); } } } diff --git a/src/debugger/gui/RomListWidget.hxx b/src/debugger/gui/RomListWidget.hxx index 40abb0adf..ec557f606 100644 --- a/src/debugger/gui/RomListWidget.hxx +++ b/src/debugger/gui/RomListWidget.hxx @@ -96,7 +96,6 @@ class RomListWidget : public EditableWidget int _currentPos{0}; // position of first line in visible window int _selectedItem{-1}; int _highlightedItem{-1}; - bool _editMode{false}; StellaKey _currentKeyDown{KBDK_UNKNOWN}; Common::Base::Fmt _base{Common::Base::Fmt::_DEFAULT}; // base used during editing diff --git a/src/debugger/gui/RomWidget.cxx b/src/debugger/gui/RomWidget.cxx index 87572d6ed..0454ef16d 100644 --- a/src/debugger/gui/RomWidget.cxx +++ b/src/debugger/gui/RomWidget.cxx @@ -90,10 +90,6 @@ void RomWidget::handleCommand(CommandSender* sender, int cmd, int data, int id) case RomListWidget::kBPointChangedCmd: // 'data' is the line in the disassemblylist to be accessed toggleBreak(data); - // Refresh the romlist, since the breakpoint may not have - // actually changed - myRomList->setDirty(); - myRomList->draw(); break; case RomListWidget::kRomChangedCmd: diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 95379398a..215289771 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -476,13 +476,17 @@ void Dialog::drawDialog() clearDirty(); } + // Draw all children + drawChain(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Dialog::drawChain() +{ Widget* w = _firstWidget; - // Draw all children - w = _firstWidget; while(w) { - // only redraw changed widgets if(w->needsRedraw()) w->draw(); w = w->_next; diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index abb22911a..de81b4a1d 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -65,6 +65,7 @@ class Dialog : public GuiObject void tick() override; bool isChainDirty() const override; void redraw(bool force = false); + void drawChain() override; void render(); void addFocusWidget(Widget* w) override; diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index 4c3928052..8e19cb0a2 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -66,7 +66,7 @@ void EditableWidget::setText(const string& str, bool) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void EditableWidget::tick() { - if(_hasFocus && _editable && isVisible() && _boss->isVisible()) + if(_hasFocus && isEditable() && _editMode && isVisible() && _boss->isVisible()) { _caretTimer++; if(_caretTimer > 40) // switch every 2/3rd seconds diff --git a/src/gui/EditableWidget.hxx b/src/gui/EditableWidget.hxx index 345a4c618..8ebf1791c 100644 --- a/src/gui/EditableWidget.hxx +++ b/src/gui/EditableWidget.hxx @@ -123,6 +123,7 @@ class EditableWidget : public Widget, public CommandSender protected: int _editScrollOffset{0}; + bool _editMode{true}; private: TextFilter _filter; diff --git a/src/gui/GuiObject.hxx b/src/gui/GuiObject.hxx index 581d498aa..893686e5f 100644 --- a/src/gui/GuiObject.hxx +++ b/src/gui/GuiObject.hxx @@ -91,16 +91,18 @@ class GuiObject : public CommandReceiver virtual void setHeight(int h) { _h = h; } virtual bool isVisible() const = 0; - virtual void setDirty() { _dirty = true; } - virtual void clearDirty() { _dirty = false; } - virtual void tick() = 0; - virtual bool isDirty() const { return _dirty; } + void setDirty() { _dirty = true; } + void clearDirty() { _dirty = false; } + bool isDirty() const { return _dirty; } virtual bool isChainDirty() const = 0; + // The GUI indicates if its underlying surface needs to be redrawn // and then re-rendered virtual bool needsRedraw() { return isDirty() || isChainDirty(); } + virtual void tick() = 0; + void setFlags(uInt32 flags) { uInt32 oldFlags = _flags; @@ -140,6 +142,7 @@ class GuiObject : public CommandReceiver protected: virtual void releaseFocus() = 0; virtual void draw() = 0; + virtual void drawChain() = 0; private: OSystem& myOSystem; diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 93331cea1..e04da70d9 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -157,7 +157,14 @@ void Widget::draw() clearDirty(); // Draw all children + drawChain(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::drawChain() +{ Widget* w = _firstWidget; + while(w) { if(w->needsRedraw()) diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 13eefdbf7..38f8fecfa 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -72,6 +72,7 @@ class Widget : public GuiObject void tick() override; bool isChainDirty() const override; void draw() override; + void drawChain() override; void receivedFocus(); void lostFocus(); void addFocusWidget(Widget* w) override { _focusList.push_back(w); } From a81ab40f5845cf212a4786b8118f53ae9668586b Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 14 Nov 2020 10:03:29 +0100 Subject: [PATCH 033/107] removed special colors and drawing for Dialog in background --- src/emucore/FrameBuffer.cxx | 10 ++++------ src/emucore/FrameBufferConstants.hxx | 4 +--- src/gui/Dialog.cxx | 15 ++++----------- src/gui/Dialog.hxx | 3 +-- 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index fce9cdae4..91cabfe3e 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -1326,8 +1326,6 @@ void FrameBuffer::toggleGrabMouse() kColorInfo TIA output position color kColorTitleBar Title bar color kColorTitleText Title text color - kColorTitleBarLo Disabled title bar color - kColorTitleTextLo Disabled title text color */ UIPaletteArray FrameBuffer::ourStandardUIPalette = { { 0x686868, 0x000000, 0xa38c61, 0xdccfa5, 0x404040, // base @@ -1338,7 +1336,7 @@ UIPaletteArray FrameBuffer::ourStandardUIPalette = { 0xac3410, 0xd55941, // scrollbar 0xc80000, 0xffff80, 0xc8c8ff, 0xc80000, // debugger 0xac3410, 0xd55941, 0xdccfa5, 0xf0f0cf, 0xa38c61, // slider - 0xffffff, 0xac3410, 0xf0f0cf, 0x686868, 0xdccfa5 // other + 0xffffff, 0xac3410, 0xf0f0cf // other } }; @@ -1351,7 +1349,7 @@ UIPaletteArray FrameBuffer::ourClassicUIPalette = { 0x20a020, 0x00ff00, // scrollbar 0xc80000, 0x00ff00, 0xc8c8ff, 0xc80000, // debugger 0x20a020, 0x00ff00, 0x404040, 0x686868, 0x404040, // slider - 0x00ff00, 0x20a020, 0x000000, 0x686868, 0x404040 // other + 0x00ff00, 0x20a020, 0x000000 // other } }; @@ -1364,7 +1362,7 @@ UIPaletteArray FrameBuffer::ourLightUIPalette = { 0xc0c0c0, 0x808080, // scrollbar 0xffc0c0, 0x000000, 0xe00000, 0xc00000, // debugger 0x333333, 0x0078d7, 0xc0c0c0, 0xffffff, 0xc0c0c0, // slider 0xBDDEF9| 0xe1e1e1 | 0xffffff - 0xffffff, 0x333333, 0xf0f0f0, 0x808080, 0xc0c0c0 // other + 0xffffff, 0x333333, 0xf0f0f0 // other } }; @@ -1377,6 +1375,6 @@ UIPaletteArray FrameBuffer::ourDarkUIPalette = { 0x3c3c3c, 0x646464, // scrollbar 0x7f2020, 0xc0c0c0, 0xe00000, 0xc00000, // debugger 0x989898, 0x0059a3, 0x3c3c3c, 0x000000, 0x3c3c3c, // slider - 0x000000, 0x989898, 0x202020, 0x646464, 0x3c3c3c // other + 0x000000, 0x989898, 0x202020 // other } }; diff --git a/src/emucore/FrameBufferConstants.hxx b/src/emucore/FrameBufferConstants.hxx index 134086a0a..7896fd04c 100644 --- a/src/emucore/FrameBufferConstants.hxx +++ b/src/emucore/FrameBufferConstants.hxx @@ -109,9 +109,7 @@ static constexpr ColorId kColorInfo = 287, kColorTitleBar = 288, kColorTitleText = 289, - kColorTitleBarLo = 290, - kColorTitleTextLo = 291, - kNumColors = 292, + kNumColors = 290, kNone = 0 // placeholder to represent default/no color ; diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 215289771..a2b9c14e3 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -441,26 +441,19 @@ void Dialog::drawDialog() { //cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; - // Dialog is still on top if e.g a ContextMenu is opened - _onTop = true/*parent().myDialogStack.top() == this*/ - || (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this - && !parent().myDialogStack.top()->hasTitle()); - - cerr << "on top " << isOnTop() << endl; if(clearsBackground()) { // cerr << "Dialog::drawDialog(): w = " << _w << ", h = " << _h << " @ " << &s << endl << endl; if(hasBackground()) - s.fillRect(_x, _y + _th, _w, _h - _th, _onTop ? kDlgColor : kBGColorLo); + s.fillRect(_x, _y + _th, _w, _h - _th, kDlgColor); else s.invalidateRect(_x, _y + _th, _w, _h - _th); if(_th) { - s.fillRect(_x, _y, _w, _th, _onTop ? kColorTitleBar : kColorTitleBarLo); + s.fillRect(_x, _y, _w, _th, kColorTitleBar); s.drawString(_font, _title, _x + _font.getMaxCharWidth() * 1.25, _y + _font.getFontHeight() / 6, - _font.getStringWidth(_title), - _onTop ? kColorTitleText : kColorTitleTextLo); + _font.getStringWidth(_title), kColorTitleText); } } else { @@ -468,7 +461,7 @@ void Dialog::drawDialog() cerr << "invalidate " << typeid(*this).name() << endl; } if(hasBorder()) // currently only used by Dialog itself - s.frameRect(_x, _y, _w, _h, _onTop ? kColor : kShadowColor); + s.frameRect(_x, _y, _w, _h, kColor); // Make all child widgets dirty Widget::setDirtyInChain(_firstWidget); diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index de81b4a1d..3966e5db9 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -54,7 +54,7 @@ class Dialog : public GuiObject void close(); bool isVisible() const override { return _visible; } - bool isOnTop() const { return _onTop; } + bool isOnTop() const { return true; } // TODO: remove virtual void setPosition(); virtual void drawDialog(); @@ -197,7 +197,6 @@ class Dialog : public GuiObject Widget* _cancelWidget{nullptr}; bool _visible{false}; - bool _onTop{true}; bool _processCancel{false}; string _title; int _th{0}; From 1c5d31db6004bc4e17dad2b42e57c51ae64f739c Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 14 Nov 2020 12:07:44 +0100 Subject: [PATCH 034/107] improved dirty chain detection --- src/gui/Dialog.cxx | 52 +++++++++++++++++++------------------- src/gui/Dialog.hxx | 6 +++-- src/gui/EditableWidget.cxx | 2 +- src/gui/GuiObject.hxx | 7 +++-- src/gui/Widget.cxx | 29 ++++++++++++--------- src/gui/Widget.hxx | 4 ++- 6 files changed, 56 insertions(+), 44 deletions(-) diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index a2b9c14e3..84c4c9dac 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -147,20 +147,15 @@ void Dialog::setPosition() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool Dialog::isChainDirty() const +void Dialog::setDirty() { - bool dirty = false; + _dirty = true; +} - // Recursively check if dialog or any chick dialogs or widgets are dirty - Widget* w = _firstWidget; - - while(w && !dirty) - { - dirty |= w->needsRedraw(); - w = w->_next; - } - - return dirty; +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Dialog::setDirtyChain() +{ + _dirtyChain = true; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -440,6 +435,7 @@ void Dialog::drawDialog() if(isDirty()) { //cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; + cerr << "d"; if(clearsBackground()) { @@ -458,7 +454,7 @@ void Dialog::drawDialog() } else { s.invalidate(); - cerr << "invalidate " << typeid(*this).name() << endl; + //cerr << "invalidate " << typeid(*this).name() << endl; } if(hasBorder()) // currently only used by Dialog itself s.frameRect(_x, _y, _w, _h, kColor); @@ -471,19 +467,6 @@ void Dialog::drawDialog() // Draw all children drawChain(); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void Dialog::drawChain() -{ - Widget* w = _firstWidget; - - while(w) - { - if(w->needsRedraw()) - w->draw(); - w = w->_next; - } // Draw outlines for focused widgets // Don't change focus, since this will trigger lost and received @@ -497,6 +480,23 @@ void Dialog::drawChain() } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Dialog::drawChain() +{ + // Clear chain *before* drawing, because some widgets may set it again when + // being drawn (e.g. RomListWidget) + clearDirtyChain(); + + Widget* w = _firstWidget; + + while(w) + { + if(w->needsRedraw()) + w->draw(); + w = w->_next; + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Dialog::handleText(char text) { diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index 3966e5db9..0d0c57bc3 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -62,12 +62,14 @@ class Dialog : public GuiObject virtual void saveConfig() { } virtual void setDefaults() { } - void tick() override; - bool isChainDirty() const override; + void setDirty() override; + void setDirtyChain() override; void redraw(bool force = false); void drawChain() override; void render(); + void tick() override; + void addFocusWidget(Widget* w) override; void addToFocusList(WidgetArray& list) override; void addToFocusList(WidgetArray& list, TabWidget* w, int tabId); diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index 8e19cb0a2..3e86ea947 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -73,7 +73,7 @@ void EditableWidget::tick() { _caretTimer = 0; _caretEnabled = !_caretEnabled; - _dirty = true; + setDirty(); } } Widget::tick(); diff --git a/src/gui/GuiObject.hxx b/src/gui/GuiObject.hxx index 893686e5f..c276698f8 100644 --- a/src/gui/GuiObject.hxx +++ b/src/gui/GuiObject.hxx @@ -92,10 +92,12 @@ class GuiObject : public CommandReceiver virtual bool isVisible() const = 0; - void setDirty() { _dirty = true; } + virtual void setDirty() = 0; + virtual void setDirtyChain() = 0; void clearDirty() { _dirty = false; } + void clearDirtyChain() { _dirtyChain = false; } bool isDirty() const { return _dirty; } - virtual bool isChainDirty() const = 0; + bool isChainDirty() const { return _dirtyChain; } // The GUI indicates if its underlying surface needs to be redrawn // and then re-rendered @@ -152,6 +154,7 @@ class GuiObject : public CommandReceiver protected: int _x{0}, _y{0}, _w{0}, _h{0}; bool _dirty{false}; + bool _dirtyChain{false}; uInt32 _flags{0}; Widget* _firstWidget{nullptr}; diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index e04da70d9..f33d27146 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -52,20 +52,21 @@ Widget::~Widget() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool Widget::isChainDirty() const +void Widget::setDirty() { - bool dirty = false; + _dirty = true; - // Recursively check if widget or any child dialogs or widgets are dirty - Widget* w = _firstWidget; + // Inform the parent object that its children chain is dirty + _boss->setDirtyChain(); +} - while(w && !dirty) - { - dirty |= w->needsRedraw(); - w = w->_next; - } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::setDirtyChain() +{ + _dirtyChain = true; - return dirty; + // Inform the parent object that its children chain is dirty + _boss->setDirtyChain(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -98,7 +99,8 @@ void Widget::draw() if(isDirty()) { - cerr << " *** draw widget " << typeid(*this).name() << " ***" << endl; + //cerr << " *** draw widget " << typeid(*this).name() << " ***" << endl; + cerr << "w"; FBSurface& s = _boss->dialog().surface(); @@ -163,6 +165,10 @@ void Widget::draw() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Widget::drawChain() { + // Clear chain *before* drawing, because some widgets may set it again when + // being drawn (e.g. RomListWidget) + clearDirtyChain(); + Widget* w = _firstWidget; while(w) @@ -508,7 +514,6 @@ void ButtonWidget::setBitmap(const uInt32* bitmap, int bmw, int bmh) _bmh = bmh; _bmw = bmw; - cerr << "setBitmap" << endl; setDirty(); } diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 38f8fecfa..5ee8ef0b1 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -70,7 +70,9 @@ class Widget : public GuiObject virtual bool handleEvent(Event::Type event) { return false; } void tick() override; - bool isChainDirty() const override; + + void setDirty() override; + void setDirtyChain() override; void draw() override; void drawChain() override; void receivedFocus(); From eca862b2403fd48a2a06b3c4dcc87bd2f979a31c Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Sat, 14 Nov 2020 20:41:06 -0330 Subject: [PATCH 035/107] Eliminate graphical garbage in background in fullscreen mode for Linux/Mac. --- src/gui/DialogContainer.cxx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 1c75273b4..e7fb0b81d 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -124,6 +124,10 @@ void DialogContainer::render() cerr << "full re-render " << typeid(*this).name() << endl; + // Make sure we start in a clean state (with zero'ed buffers) + if(!myOSystem.eventHandler().inTIAMode()) + myOSystem.frameBuffer().clear(); + // Render all dialogs myDialogStack.applyAll([&](Dialog*& d) { d->render(); From a030bc30b890d1252a60b25720e2e335a97fc552 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Sat, 14 Nov 2020 22:35:05 -0330 Subject: [PATCH 036/107] Optimize/simplify dialog shading slightly - move creation to c'tor - apply position and size with one method instead of two --- src/common/FBSurfaceSDL2.cxx | 40 ++++++++++++++---------- src/common/FBSurfaceSDL2.hxx | 49 ++++++++++++++++++++++-------- src/emucore/FBSurface.hxx | 3 ++ src/gui/Dialog.cxx | 33 +++++++++----------- src/libretro/FBSurfaceLIBRETRO.hxx | 3 ++ 5 files changed, 80 insertions(+), 48 deletions(-) diff --git a/src/common/FBSurfaceSDL2.cxx b/src/common/FBSurfaceSDL2.cxx index 398c773ba..7f0ddb7d7 100644 --- a/src/common/FBSurfaceSDL2.cxx +++ b/src/common/FBSurfaceSDL2.cxx @@ -104,41 +104,49 @@ const Common::Rect& FBSurfaceSDL2::dstRect() const // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FBSurfaceSDL2::setSrcPos(uInt32 x, uInt32 y) { - if(x != static_cast(mySrcR.x) || y != static_cast(mySrcR.y)) - { - setSrcPosInternal(x, y); + if(setSrcPosInternal(x, y)) reinitializeBlitter(); - } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FBSurfaceSDL2::setSrcSize(uInt32 w, uInt32 h) { - if(w != static_cast(mySrcR.w) || h != static_cast(mySrcR.h)) - { - setSrcSizeInternal(w, h); + if(setSrcSizeInternal(w, h)) + reinitializeBlitter(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void FBSurfaceSDL2::setSrcRect(const Common::Rect& r) +{ + const bool posChanged = setSrcPosInternal(r.x(), r.y()), + sizeChanged = setSrcSizeInternal(r.w(), r.h()); + + if(posChanged || sizeChanged) reinitializeBlitter(); - } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FBSurfaceSDL2::setDstPos(uInt32 x, uInt32 y) { - if(x != static_cast(myDstR.x) || y != static_cast(myDstR.y)) - { - setDstPosInternal(x, y); + if(setDstPosInternal(x, y)) reinitializeBlitter(); - } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FBSurfaceSDL2::setDstSize(uInt32 w, uInt32 h) { - if(w != static_cast(myDstR.w) || h != static_cast(myDstR.h)) - { - setDstSizeInternal(w, h); + if(setDstSizeInternal(w, h)) + reinitializeBlitter(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void FBSurfaceSDL2::setDstRect(const Common::Rect& r) +{ + const bool posChanged = setDstPosInternal(r.x(), r.y()), + sizeChanged = setDstSizeInternal(r.w(), r.h()); + + if(posChanged || sizeChanged) reinitializeBlitter(); - } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/common/FBSurfaceSDL2.hxx b/src/common/FBSurfaceSDL2.hxx index c19808f27..d8bfa0029 100644 --- a/src/common/FBSurfaceSDL2.hxx +++ b/src/common/FBSurfaceSDL2.hxx @@ -48,8 +48,11 @@ class FBSurfaceSDL2 : public FBSurface const Common::Rect& dstRect() const override; void setSrcPos(uInt32 x, uInt32 y) override; void setSrcSize(uInt32 w, uInt32 h) override; + void setSrcRect(const Common::Rect& r) override; void setDstPos(uInt32 x, uInt32 y) override; void setDstSize(uInt32 w, uInt32 h) override; + void setDstRect(const Common::Rect& r) override; + void setVisible(bool visible) override; void translateCoords(Int32& x, Int32& y) const override; @@ -67,21 +70,41 @@ class FBSurfaceSDL2 : public FBSurface void applyAttributes() override; private: - inline void setSrcPosInternal(uInt32 x, uInt32 y) { - mySrcR.x = x; mySrcR.y = y; - mySrcGUIR.moveTo(x, y); + inline bool setSrcPosInternal(uInt32 x, uInt32 y) { + if(x != static_cast(mySrcR.x) || y != static_cast(mySrcR.y)) + { + mySrcR.x = x; mySrcR.y = y; + mySrcGUIR.moveTo(x, y); + return true; + } + return false; } - inline void setSrcSizeInternal(uInt32 w, uInt32 h) { - mySrcR.w = w; mySrcR.h = h; - mySrcGUIR.setWidth(w); mySrcGUIR.setHeight(h); + inline bool setSrcSizeInternal(uInt32 w, uInt32 h) { + if(w != static_cast(mySrcR.w) || h != static_cast(mySrcR.h)) + { + mySrcR.w = w; mySrcR.h = h; + mySrcGUIR.setWidth(w); mySrcGUIR.setHeight(h); + return true; + } + return false; } - inline void setDstPosInternal(uInt32 x, uInt32 y) { - myDstR.x = x; myDstR.y = y; - myDstGUIR.moveTo(x, y); + inline bool setDstPosInternal(uInt32 x, uInt32 y) { + if(x != static_cast(myDstR.x) || y != static_cast(myDstR.y)) + { + myDstR.x = x; myDstR.y = y; + myDstGUIR.moveTo(x, y); + return true; + } + return false; } - inline void setDstSizeInternal(uInt32 w, uInt32 h) { - myDstR.w = w; myDstR.h = h; - myDstGUIR.setWidth(w); myDstGUIR.setHeight(h); + inline bool setDstSizeInternal(uInt32 w, uInt32 h) { + if(w != static_cast(myDstR.w) || h != static_cast(myDstR.h)) + { + myDstR.w = w; myDstR.h = h; + myDstGUIR.setWidth(w); myDstGUIR.setHeight(h); + return true; + } + return false; } void createSurface(uInt32 width, uInt32 height, const uInt32* data); @@ -103,7 +126,7 @@ class FBSurfaceSDL2 : public FBSurface {ScalingInterpolation::none}; SDL_Surface* mySurface{nullptr}; - SDL_Rect mySrcR{0, 0, 0, 0}, myDstR{0, 0, 0, 0}; + SDL_Rect mySrcR{-1, -1, -1, -1}, myDstR{-1, -1, -1, -1}; bool myIsVisible{true}; bool myIsStatic{false}; diff --git a/src/emucore/FBSurface.hxx b/src/emucore/FBSurface.hxx index ea851e6e6..71bddc289 100644 --- a/src/emucore/FBSurface.hxx +++ b/src/emucore/FBSurface.hxx @@ -292,11 +292,14 @@ class FBSurface These methods set the origin point and width/height for the specified service. They are defined as separate x/y and w/h methods since these items are sometimes set separately. + Other times they are set together, so we can use a Rect instead. */ virtual void setSrcPos(uInt32 x, uInt32 y) = 0; virtual void setSrcSize(uInt32 w, uInt32 h) = 0; + virtual void setSrcRect(const Common::Rect& r) = 0; virtual void setDstPos(uInt32 x, uInt32 y) = 0; virtual void setDstSize(uInt32 w, uInt32 h) = 0; + virtual void setDstRect(const Common::Rect& r) = 0; /** This method should be called to enable/disable showing the surface diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 84c4c9dac..6a0e96899 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -53,6 +53,18 @@ Dialog::Dialog(OSystem& instance, DialogContainer& parent, const GUI::Font& font { _flags = Widget::FLAG_ENABLED | Widget::FLAG_BORDER | Widget::FLAG_CLEARBG; setTitle(title); + + // Create shading surface + uInt32 data = 0xff000000; + + _shadeSurface = instance.frameBuffer().allocateSurface( + 1, 1, ScalingInterpolation::sharp, &data); + + FBSurface::Attributes& attr = _shadeSurface->attributes(); + + attr.blending = true; + attr.blendalpha = 25; // darken background dialogs by 25% + _shadeSurface->applyAttributes(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -223,7 +235,7 @@ void Dialog::redraw(bool force) // Draw this dialog setPosition(); drawDialog(); - // full rendering is caused in dialog container + // full rendering is caused in dialog container } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -251,24 +263,7 @@ void Dialog::render() { cerr << " shade " << typeid(*this).name() << endl; - if(_shadeSurface == nullptr) - { - uInt32 data = 0xff000000; - - _shadeSurface = instance().frameBuffer().allocateSurface( - 1, 1, ScalingInterpolation::sharp, &data); - - FBSurface::Attributes& attr = _shadeSurface->attributes(); - - attr.blending = true; - attr.blendalpha = 25; // darken background dialogs by 25% - _shadeSurface->applyAttributes(); - } - - const Common::Rect& rect = _surface->dstRect(); - _shadeSurface->setDstPos(rect.x(), rect.y()); - _shadeSurface->setDstSize(rect.w(), rect.h()); - + _shadeSurface->setDstRect(_surface->dstRect()); _shadeSurface->render(); } } diff --git a/src/libretro/FBSurfaceLIBRETRO.hxx b/src/libretro/FBSurfaceLIBRETRO.hxx index 3918ff673..f6bf3320b 100644 --- a/src/libretro/FBSurfaceLIBRETRO.hxx +++ b/src/libretro/FBSurfaceLIBRETRO.hxx @@ -44,8 +44,11 @@ class FBSurfaceLIBRETRO : public FBSurface const Common::Rect& dstRect() const override { return myDstGUIR; } void setSrcPos(uInt32 x, uInt32 y) override { } void setSrcSize(uInt32 w, uInt32 h) override { } + void setSrcRect(const Common::Rect& r) override { } void setDstPos(uInt32 x, uInt32 y) override { } void setDstSize(uInt32 w, uInt32 h) override { } + void setDstRect(const Common::Rect& r) override { } + void setVisible(bool visible) override { } void translateCoords(Int32& x, Int32& y) const override { } From a65e6eab99066fccf1dd062a758e63973202afb8 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sun, 15 Nov 2020 08:59:18 +0100 Subject: [PATCH 037/107] fixed garbage in fullscreen mode fixed breakpoints flickering in RomListWidget fixed palette update in VideoAudioDialog --- src/debugger/gui/RomListWidget.cxx | 2 ++ src/gui/DialogContainer.cxx | 4 ++++ src/gui/VideoAudioDialog.cxx | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 3504b2b37..473df388e 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -491,6 +491,8 @@ void RomListWidget::drawWidget(bool hilite) checkBreakPoint(dlist[pos].address, instance().debugger().cartDebug().getBank(dlist[pos].address))); myCheckList[i]->setDirty(); + // draw immediately, because chain order is not deterministic + myCheckList[i]->draw(); // Draw highlighted item in a frame if(_highlightedItem == pos) diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index e7fb0b81d..0f7713d0d 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -122,6 +122,10 @@ void DialogContainer::render() if(myDialogStack.empty()) return; + // Make sure we start in a clean state (with zero'ed buffers) + if(!myOSystem.eventHandler().inTIAMode()) + myOSystem.frameBuffer().clear(); + cerr << "full re-render " << typeid(*this).name() << endl; // Make sure we start in a clean state (with zero'ed buffers) diff --git a/src/gui/VideoAudioDialog.cxx b/src/gui/VideoAudioDialog.cxx index ba141cbeb..86db273b9 100644 --- a/src/gui/VideoAudioDialog.cxx +++ b/src/gui/VideoAudioDialog.cxx @@ -959,7 +959,16 @@ void VideoAudioDialog::handlePaletteUpdate() instance().frameBuffer().tiaSurface().paletteHandler().setAdjustables(paletteAdj); if(instance().hasConsole()) + { instance().frameBuffer().tiaSurface().paletteHandler().setPalette(); + + constexpr int NUM_LUMA = 8; + constexpr int NUM_CHROMA = 16; + + for(int idx = 0; idx < NUM_CHROMA; ++idx) + for(int lum = 0; lum < NUM_LUMA; ++lum) + myColor[idx][lum]->setDirty(); + } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From ccdd167fca19ff8d4f4cac800f2d8154a19cf7e1 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sun, 15 Nov 2020 11:03:55 +0100 Subject: [PATCH 038/107] removed duplicate _editMode variable fixed missing redraws when StringListWidgets gain focus prevent focus for disabled widget --- src/gui/Dialog.cxx | 4 ++-- src/gui/DialogContainer.cxx | 6 +----- src/gui/EditableWidget.cxx | 2 -- src/gui/ListWidget.cxx | 2 ++ src/gui/ListWidget.hxx | 1 - src/gui/OptionsDialog.hxx | 2 +- src/gui/StringListWidget.hxx | 3 +++ 7 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 6a0e96899..18dd20cbf 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -361,7 +361,7 @@ void Dialog::setFocus(Widget* w) { // If the click occured inside a widget which is not the currently // focused one, change the focus to that widget. - if(w && w != _focusedWidget && w->wantsFocus()) + if(w && w != _focusedWidget && w->wantsFocus() && w->isEnabled()) { // Redraw widgets for new focus _focusedWidget = Widget::setFocusForChain(this, getFocusList(), w, 0); @@ -427,10 +427,10 @@ void Dialog::drawDialog() FBSurface& s = surface(); + cerr << endl << "d"; if(isDirty()) { //cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; - cerr << "d"; if(clearsBackground()) { diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 0f7713d0d..fcdac91a9 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -122,10 +122,6 @@ void DialogContainer::render() if(myDialogStack.empty()) return; - // Make sure we start in a clean state (with zero'ed buffers) - if(!myOSystem.eventHandler().inTIAMode()) - myOSystem.frameBuffer().clear(); - cerr << "full re-render " << typeid(*this).name() << endl; // Make sure we start in a clean state (with zero'ed buffers) @@ -174,7 +170,7 @@ void DialogContainer::removeDialog() { if(!myDialogStack.empty()) { - cerr << "remove dialog" << endl; + cerr << "remove dialog " << typeid(*myDialogStack.top()).name() << endl; myDialogStack.pop(); // Inform the frame buffer that it has to render all surfaces diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index 3e86ea947..d6c35fa55 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -100,8 +100,6 @@ void EditableWidget::receivedFocusWidget() { _caretTimer = 0; _caretEnabled = true; - - Widget::receivedFocusWidget(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/ListWidget.cxx b/src/gui/ListWidget.cxx index d8b0bc458..1cf6c7e0d 100644 --- a/src/gui/ListWidget.cxx +++ b/src/gui/ListWidget.cxx @@ -36,6 +36,8 @@ ListWidget::ListWidget(GuiObject* boss, const GUI::Font& font, _textcolor = kTextColor; _textcolorhi = kTextColor; + _editMode = false; + _cols = w / _fontWidth; _rows = h / _lineHeight; diff --git a/src/gui/ListWidget.hxx b/src/gui/ListWidget.hxx index 96d4f036c..1fba03f01 100644 --- a/src/gui/ListWidget.hxx +++ b/src/gui/ListWidget.hxx @@ -99,7 +99,6 @@ class ListWidget : public EditableWidget int _currentPos{0}; int _selectedItem{-1}; int _highlightedItem{-1}; - bool _editMode{false}; bool _useScrollbar{true}; ScrollBarWidget* _scrollBar{nullptr}; diff --git a/src/gui/OptionsDialog.hxx b/src/gui/OptionsDialog.hxx index 55d4d4b0d..bd13c3f37 100644 --- a/src/gui/OptionsDialog.hxx +++ b/src/gui/OptionsDialog.hxx @@ -52,7 +52,7 @@ class OptionsDialog : public Dialog void handleCommand(CommandSender* sender, int cmd, int data, int id) override; private: - unique_ptr myVideoDialog; + unique_ptr myVideoDialog; unique_ptr myEmulationDialog; unique_ptr myInputDialog; unique_ptr myUIDialog; diff --git a/src/gui/StringListWidget.hxx b/src/gui/StringListWidget.hxx index eed279aeb..ee3b56e97 100644 --- a/src/gui/StringListWidget.hxx +++ b/src/gui/StringListWidget.hxx @@ -35,6 +35,9 @@ class StringListWidget : public ListWidget protected: void handleMouseEntered() override; void handleMouseLeft() override; + // display depends on _hasFocus so we have to redraw when focus changes + void receivedFocusWidget() override { setDirty(); } + void lostFocusWidget() override { setDirty(); } void drawWidget(bool hilite) override; Common::Rect getEditRect() const override; From f4b23967975d643e8f8ffd89b07a5125aaa2aab4 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sun, 15 Nov 2020 16:41:01 +0100 Subject: [PATCH 039/107] attempt to fix 'shifting' dialogs (OptionsDialog) --- src/emucore/EventHandler.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/emucore/EventHandler.cxx b/src/emucore/EventHandler.cxx index 7d241101e..5eced0d94 100644 --- a/src/emucore/EventHandler.cxx +++ b/src/emucore/EventHandler.cxx @@ -2288,6 +2288,7 @@ void EventHandler::enterMenuMode(EventHandlerState state) void EventHandler::leaveMenuMode() { #ifdef GUI_SUPPORT + myOverlay->removeDialog(); // remove the base dialog from dialog stack setState(EventHandlerState::EMULATION); myOSystem.sound().mute(false); #endif From c3530863b5b4e5639aaa52bc359cccf296773966 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Sun, 15 Nov 2020 15:16:06 -0330 Subject: [PATCH 040/107] Some simplifications to Point/Size/Rect classes. --- src/common/Rect.hxx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/common/Rect.hxx b/src/common/Rect.hxx index faf6677b6..708de9d86 100644 --- a/src/common/Rect.hxx +++ b/src/common/Rect.hxx @@ -44,8 +44,8 @@ struct Point if(c != 'x') x = y = 0; } - bool operator==(const Point & p) const { return x == p.x && y == p.y; } - bool operator!=(const Point & p) const { return x != p.x || y != p.y; } + bool operator==(const Point& p) const { return x == p.x && y == p.y; } + bool operator!=(const Point& p) const { return !(*this == p); } friend ostream& operator<<(ostream& os, const Point& p) { os << p.x << "x" << p.y; @@ -75,11 +75,11 @@ struct Size } bool operator==(const Size& s) const { return w == s.w && h == s.h; } - bool operator!=(const Size& s) const { return w != s.w || h != s.h; } - bool operator<(const Size& s) const { return w < s.w && h < s.h; } - bool operator<=(const Size& s) const { return w <= s.w && h <= s.h; } - bool operator>(const Size& s) const { return w > s.w || h > s.h; } - bool operator>=(const Size& s) const { return w >= s.w || h >= s.h; } + bool operator< (const Size& s) const { return w < s.w && h < s.h; } + bool operator> (const Size& s) const { return w > s.w || h > s.h; } + bool operator!=(const Size& s) const { return !(*this == s); } + bool operator<=(const Size& s) const { return !(*this > s); } + bool operator>=(const Size& s) const { return !(*this < s); } friend ostream& operator<<(ostream& os, const Size& s) { os << s.w << "x" << s.h; @@ -175,6 +175,11 @@ struct Rect return r.left != x || r.top != y; } + bool operator==(const Rect& r) const { + return top == r.top && left == r.left && bottom == r.bottom && right == r.right; + } + bool operator!=(const Rect& r) const { return !(*this == r); } + friend ostream& operator<<(ostream& os, const Rect& r) { os << r.point() << "," << r.size(); return os; From 7144ff496493248b2306e43324d291cacd5f9d7b Mon Sep 17 00:00:00 2001 From: Christian Speckner Date: Sun, 15 Nov 2020 19:55:35 +0100 Subject: [PATCH 041/107] Enable rtti in makefile. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 168ae178d..021e802dc 100644 --- a/Makefile +++ b/Makefile @@ -48,11 +48,11 @@ endif CXXFLAGS+= -Wall -Wextra -Wno-unused-parameter ifdef HAVE_GCC - CXXFLAGS+= -Wno-multichar -Wunused -fno-rtti -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 + CXXFLAGS+= -Wno-multichar -Wunused -fno-rtti -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 -frtti endif ifdef HAVE_CLANG - CXXFLAGS+= -Wno-multichar -Wunused -fno-rtti -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 + CXXFLAGS+= -Wno-multichar -Wunused -fno-rtti -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 -frtti endif ifdef CLANG_WARNINGS From d9e23fd9ebdfe94d76a3196413b5304b4b1fb007 Mon Sep 17 00:00:00 2001 From: Christian Speckner Date: Sun, 15 Nov 2020 23:16:26 +0100 Subject: [PATCH 042/107] Remove overkill. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 021e802dc..90279a56a 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ ifdef HAVE_GCC endif ifdef HAVE_CLANG - CXXFLAGS+= -Wno-multichar -Wunused -fno-rtti -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 -frtti + CXXFLAGS+= -Wno-multichar -Wunused -frtti -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 endif ifdef CLANG_WARNINGS From 03311a4b76b6b733798cf74ed5b161086c44b653 Mon Sep 17 00:00:00 2001 From: cd-w Date: Sun, 15 Nov 2020 15:26:03 -0800 Subject: [PATCH 043/107] Increase sample size from 2K to 512K for CDFJ+ --- src/emucore/CartCDF.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emucore/CartCDF.cxx b/src/emucore/CartCDF.cxx index 93e330c39..43c7290ac 100644 --- a/src/emucore/CartCDF.cxx +++ b/src/emucore/CartCDF.cxx @@ -277,7 +277,7 @@ uInt8 CartridgeCDF::peek(uInt16 address) if DIGITAL_AUDIO_ON { // retrieve packed sample (max size is 2K, or 4K of unpacked data) - uInt32 sampleaddress = getSample() + (myMusicCounters[0] >> 21); + uInt32 sampleaddress = getSample() + (myMusicCounters[0] >> 13); // get sample value from ROM or RAM if (sampleaddress < 0x00080000) @@ -288,7 +288,7 @@ uInt8 CartridgeCDF::peek(uInt16 address) peekvalue = 0; // make sure current volume value is in the lower nybble - if ((myMusicCounters[0] & (1<<20)) == 0) + if ((myMusicCounters[0] & (1<<12)) == 0) peekvalue >>= 4; peekvalue &= 0x0f; } From b4d0be6461eb3994ddbe88ff7d0412290bc30b91 Mon Sep 17 00:00:00 2001 From: cd-w Date: Sun, 15 Nov 2020 15:36:23 -0800 Subject: [PATCH 044/107] Fix audio changes to be compatible with CDF/CDFJ --- src/emucore/CartCDF.cxx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emucore/CartCDF.cxx b/src/emucore/CartCDF.cxx index 43c7290ac..ae06bdc05 100644 --- a/src/emucore/CartCDF.cxx +++ b/src/emucore/CartCDF.cxx @@ -277,7 +277,8 @@ uInt8 CartridgeCDF::peek(uInt16 address) if DIGITAL_AUDIO_ON { // retrieve packed sample (max size is 2K, or 4K of unpacked data) - uInt32 sampleaddress = getSample() + (myMusicCounters[0] >> 13); + + uInt32 sampleaddress = getSample() + (myMusicCounters[0] >> (isCDFJplus() ? 13 : 21)); // get sample value from ROM or RAM if (sampleaddress < 0x00080000) @@ -288,7 +289,7 @@ uInt8 CartridgeCDF::peek(uInt16 address) peekvalue = 0; // make sure current volume value is in the lower nybble - if ((myMusicCounters[0] & (1<<12)) == 0) + if ((myMusicCounters[0] & (1<<(isCDFJplus() ? 12 : 20))) == 0) peekvalue >>= 4; peekvalue &= 0x0f; } From 120c3062864f208ddbb2f385e50deeef3dbbcd1b Mon Sep 17 00:00:00 2001 From: thrust26 Date: Mon, 16 Nov 2020 12:26:01 +0100 Subject: [PATCH 045/107] added initial tool tip functionality removed duplicate _editMode in DataGridWidget --- src/debugger/gui/DataGridWidget.cxx | 1 + src/debugger/gui/DataGridWidget.hxx | 1 - src/gui/Dialog.cxx | 8 +++ src/gui/Dialog.hxx | 4 +- src/gui/EditTextWidget.cxx | 1 + src/gui/LauncherDialog.cxx | 3 ++ src/gui/ToolTip.cxx | 79 ++++++++++++++++++++++++----- src/gui/ToolTip.hxx | 60 ++++++++++++++++++---- src/gui/Widget.cxx | 12 ++++- src/gui/Widget.hxx | 4 +- 10 files changed, 147 insertions(+), 26 deletions(-) diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index 32358dd05..877d96c6a 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -45,6 +45,7 @@ DataGridWidget::DataGridWidget(GuiObject* boss, const GUI::Font& font, _base(base) { _flags = Widget::FLAG_ENABLED | Widget::FLAG_RETAIN_FOCUS | Widget::FLAG_WANTS_RAWDATA; + _editMode = false; // Make sure all lists contain some default values _hiliteList.clear(); diff --git a/src/debugger/gui/DataGridWidget.hxx b/src/debugger/gui/DataGridWidget.hxx index 114097b01..82eca7fbc 100644 --- a/src/debugger/gui/DataGridWidget.hxx +++ b/src/debugger/gui/DataGridWidget.hxx @@ -128,7 +128,6 @@ class DataGridWidget : public EditableWidget BoolArray _changedList; BoolArray _hiliteList; - bool _editMode{false}; int _selectedItem{0}; StellaKey _currentKeyDown{KBDK_UNKNOWN}; string _backupString; diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 18dd20cbf..f58d98536 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -27,6 +27,7 @@ #include "Dialog.hxx" #include "Widget.hxx" #include "TabWidget.hxx" +#include "ToolTip.hxx" #include "ContextMenu.hxx" #include "PopUpWidget.hxx" @@ -65,6 +66,8 @@ Dialog::Dialog(OSystem& instance, DialogContainer& parent, const GUI::Font& font attr.blending = true; attr.blendalpha = 25; // darken background dialogs by 25% _shadeSurface->applyAttributes(); + + _toolTip = make_unique(instance, *this, font); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -266,6 +269,8 @@ void Dialog::render() _shadeSurface->setDstRect(_surface->dstRect()); _shadeSurface->render(); } + + _toolTip->render(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -588,6 +593,9 @@ void Dialog::handleMouseMoved(int x, int y) { Widget* w; + // Update mouse coordinates for tooltips + _toolTip->update(x, y); + if(_focusedWidget && !_dragWidget) { w = _focusedWidget; diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index 0d0c57bc3..b92b8e383 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -26,6 +26,7 @@ class OSystem; class DialogContainer; class TabWidget; class CommandSender; +class ToolTip; #include "Stack.hxx" #include "Widget.hxx" @@ -122,6 +123,7 @@ class Dialog : public GuiObject */ bool shouldResize(uInt32& w, uInt32& h) const; + ToolTip& tooltip() { return *_toolTip; }; //bool enableToolTip(); //void showToolTip(int x, int y); //void hideToolTip(); @@ -203,7 +205,7 @@ class Dialog : public GuiObject string _title; int _th{0}; int _layer{0}; - int _toolTipTimer{0}; + unique_ptr _toolTip; Common::FixedStack> mySurfaceStack; diff --git a/src/gui/EditTextWidget.cxx b/src/gui/EditTextWidget.cxx index c818f3cf0..d3e4587a5 100644 --- a/src/gui/EditTextWidget.cxx +++ b/src/gui/EditTextWidget.cxx @@ -18,6 +18,7 @@ #include "OSystem.hxx" #include "FBSurface.hxx" #include "Dialog.hxx" +#include "ToolTip.hxx" #include "Font.hxx" #include "EditTextWidget.hxx" diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 8018bcb44..abae25742 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -120,12 +120,14 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, // Show the filter input field xpos -= fwidth + LBL_GAP; myPattern = new EditTextWidget(this, font, xpos, ypos - 2, fwidth, lineHeight, ""); + myPattern->setToolTip("Enter a filter text to reduce file list."); // Show the "Filter" label xpos -= lwidth3 + LBL_GAP; new StaticTextWidget(this, font, xpos, ypos, lblFilter); // Show the checkbox for all files xpos -= lwidth2 + LBL_GAP * 3; myAllFiles = new CheckboxWidget(this, font, xpos, ypos, lblAllFiles, kAllfilesCmd); + myAllFiles->setToolTip("Uncheck to show ROM files only."); wid.push_back(myAllFiles); wid.push_back(myPattern); } @@ -178,6 +180,7 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, #ifndef BSPF_MACOS myStartButton = new ButtonWidget(this, font, xpos, ypos, (buttonWidth + 0) / 4, buttonHeight, "Select", kLoadROMCmd); + myStartButton->setToolTip("Start emulation of selected ROM."); wid.push_back(myStartButton); xpos += (buttonWidth + 0) / 4 + BUTTON_GAP; diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 53cbea0c3..72ecb47ad 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -16,25 +16,80 @@ //============================================================================ #include "OSystem.hxx" +#include "FrameBuffer.hxx" +#include "FBSurface.hxx" + #include "Font.hxx" #include "Dialog.hxx" -#include "DialogContainer.hxx" +#include "Widget.hxx" #include "ToolTip.hxx" // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -ToolTip::ToolTip(OSystem& instance, DialogContainer& parent, - const GUI::Font& font) - : Dialog(instance, parent, font) +ToolTip::ToolTip(OSystem& instance, Dialog& dialog, const GUI::Font& font) + : myDialog(dialog), + myFont(font) { - const int lineHeight = font.getLineHeight(), - fontWidth = font.getMaxCharWidth(), - fontHeight = font.getFontHeight(), - buttonWidth = font.getStringWidth("Previous") + fontWidth * 2.5, - buttonHeight = font.getLineHeight() * 1.25; - const int VBORDER = fontHeight / 2; - const int HBORDER = fontWidth * 1.25; - const int VGAP = fontHeight / 4; + const int fontWidth = font.getMaxCharWidth(), + fontHeight = font.getFontHeight(); + + myTextXOfs = fontHeight < 24 ? 5 : 8; //3 : 5; + myWidth = myTextXOfs * 2 + fontWidth * MAX_LEN; + myHeight = fontHeight + TEXT_Y_OFS * 2; + mySurface = instance.frameBuffer().allocateSurface(myWidth, myHeight); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ToolTip::request(Widget* widget) +{ + if(myWidget != widget) + { + release(); + } + if(myTimer == DELAY_TIME) + { + string text = widget->getToolTip(); + int width = std::min(myWidth, myFont.getStringWidth(text) + myTextXOfs * 2); + int yOffset = 20; // TODO: query cursor height + + myWidget = widget; + // TODO: limit to app or screen size + mySurface->setSrcSize(width, myHeight); + mySurface->setDstSize(width, myHeight); + mySurface->setDstPos(myPos.x, myPos.y + yOffset); + + mySurface->frameRect(0, 0, width, myHeight, kColor); + mySurface->fillRect(1, 1, width - 2, myHeight - 2, kWidColor); + mySurface->drawString(myFont, text, myTextXOfs, TEXT_Y_OFS, + width - myTextXOfs * 2, myHeight - TEXT_Y_OFS * 2, kTextColor); + myDialog.setDirtyChain(); + } + myTimer++; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ToolTip::release() +{ + if(myWidget != nullptr) + { + myTimer = 0; + myWidget = nullptr; + myDialog.setDirtyChain(); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ToolTip::render() +{ + if(myWidget != nullptr) + mySurface->render(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ToolTip::update(int x, int y) +{ + if(myWidget != nullptr && x != myPos.x || y != myPos.y) + release(); + myPos.x = x; + myPos.y = y; +} diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx index 2066bd3df..7ba4dc044 100644 --- a/src/gui/ToolTip.hxx +++ b/src/gui/ToolTip.hxx @@ -18,22 +18,62 @@ #ifndef TOOL_TIP_HXX #define TOOL_TIP_HXX -class OSystem; -class DialogContainer; - /** * Class for providing tooltip functionality * * @author Thomas Jentzsch */ -class ToolTip : public Dialog -{ - public: - public: - ToolTip(OSystem& instance, DialogContainer& parent, - const GUI::Font& font); - ~ToolTip() override = default; +class OSystem; +class FBSurface; +class Widget; + +#include "Rect.hxx" + +class ToolTip +{ +public: + // Maximum tooltip length + static constexpr int MAX_LEN = 60; + + ToolTip(OSystem& instance, Dialog& dialog, const GUI::Font& font); + ~ToolTip() = default; + + /** + Request a tooltip display + */ + void request(Widget* widget); + + + /** + Hide an existing tooltip (if displayed) + */ + void release(); + + /** + Update with current mouse position + */ + void update(int x, int y); + + /* + Render the tooltip + */ + void render(); + +private: + static constexpr uInt32 DELAY_TIME = 45; // display delay + static constexpr int TEXT_Y_OFS = 2; + + const GUI::Font& myFont; + Dialog& myDialog; + + Widget* myWidget{nullptr}; + uInt32 myTimer{0}; + Common::Point myPos; + int myWidth{0}; + int myHeight{0}; + int myTextXOfs{0}; + shared_ptr mySurface; }; #endif diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index f33d27146..f37f8840c 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -21,6 +21,7 @@ #include "bspf.hxx" #include "Command.hxx" #include "Dialog.hxx" +#include "ToolTip.hxx" #include "FBSurface.hxx" #include "GuiObject.hxx" #include "OSystem.hxx" @@ -74,7 +75,8 @@ void Widget::tick() { if(isEnabled()) { - //if(_hasFocus && hasToolTip()) + if(isHighlighted() && hasToolTip()) + dialog().tooltip().request(this); //{ // if(dialog().enableToolTip()) // dialog().showToolTip(10, 10); @@ -208,6 +210,14 @@ void Widget::setEnabled(bool e) else clearFlags(Widget::FLAG_ENABLED); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::setToolTip(const string& text) +{ + assert(text.length() <= ToolTip::MAX_LEN); + + _toolTipText = text; +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Widget* Widget::findWidgetInChain(Widget* w, int x, int y) { diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 5ee8ef0b1..98e36863f 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -87,6 +87,7 @@ class Widget : public GuiObject bool isEnabled() const { return _flags & FLAG_ENABLED; } bool isVisible() const override { return !(_flags & FLAG_INVISIBLE); } + bool isHighlighted() const { return _flags & FLAG_HILITED; } virtual bool wantsFocus() const { return _flags & FLAG_RETAIN_FOCUS; } bool wantsTab() const { return _flags & FLAG_WANTS_TAB; } bool wantsRaw() const { return _flags & FLAG_WANTS_RAWDATA; } @@ -102,7 +103,8 @@ class Widget : public GuiObject void setBGColorHi(ColorId color) { _bgcolorhi = color; setDirty(); } void setShadowColor(ColorId color) { _shadowcolor = color; setDirty(); } - void setToolTip(const string& text) { _toolTipText = text; } + void setToolTip(const string& text); + const string& getToolTip() const { return _toolTipText; } bool hasToolTip() const { return !_toolTipText.empty(); } virtual void loadConfig() { } From f55931f2e002b49002e42736de173427fca02366 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Mon, 16 Nov 2020 09:50:50 -0330 Subject: [PATCH 046/107] Fix warning, and add ToolTip to Linux build. --- src/gui/Dialog.hxx | 2 +- src/gui/module.mk | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index b92b8e383..b134d7d37 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -123,7 +123,7 @@ class Dialog : public GuiObject */ bool shouldResize(uInt32& w, uInt32& h) const; - ToolTip& tooltip() { return *_toolTip; }; + ToolTip& tooltip() { return *_toolTip; } //bool enableToolTip(); //void showToolTip(int x, int y); //void hideToolTip(); diff --git a/src/gui/module.mk b/src/gui/module.mk index a2f8489fd..4be9b66ad 100644 --- a/src/gui/module.mk +++ b/src/gui/module.mk @@ -49,6 +49,7 @@ MODULE_OBJS := \ src/gui/TimeLineWidget.o \ src/gui/TimeMachineDialog.o \ src/gui/TimeMachine.o \ + src/gui/ToolTip.o \ src/gui/UndoHandler.o \ src/gui/UIDialog.o \ src/gui/VideoAudioDialog.o \ From b68a6fa600826817bd27ab0d61014d78d6a653f7 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Mon, 16 Nov 2020 17:41:24 +0100 Subject: [PATCH 047/107] fixed tool tips for HiDPI added tool tip repositioning if exceeding surface --- src/gui/ToolTip.cxx | 34 +++++++++++++++++++++++++--------- src/gui/ToolTip.hxx | 14 ++++++++------ src/gui/VideoAudioDialog.cxx | 1 + 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 72ecb47ad..30100009a 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -16,11 +16,10 @@ //============================================================================ #include "OSystem.hxx" +#include "Dialog.hxx" +#include "Font.hxx" #include "FrameBuffer.hxx" #include "FBSurface.hxx" - -#include "Font.hxx" -#include "Dialog.hxx" #include "Widget.hxx" #include "ToolTip.hxx" @@ -36,7 +35,8 @@ ToolTip::ToolTip(OSystem& instance, Dialog& dialog, const GUI::Font& font) myTextXOfs = fontHeight < 24 ? 5 : 8; //3 : 5; myWidth = myTextXOfs * 2 + fontWidth * MAX_LEN; myHeight = fontHeight + TEXT_Y_OFS * 2; - mySurface = instance.frameBuffer().allocateSurface(myWidth, myHeight); + mySurface = myDialog.instance().frameBuffer().allocateSurface(myWidth, myHeight); + myScale = myDialog.instance().frameBuffer().hidpiScaleFactor(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -48,15 +48,31 @@ void ToolTip::request(Widget* widget) } if(myTimer == DELAY_TIME) { + const uInt32 VGAP = 1; + const uInt32 hCursor = 19; // TODO: query cursor height string text = widget->getToolTip(); - int width = std::min(myWidth, myFont.getStringWidth(text) + myTextXOfs * 2); - int yOffset = 20; // TODO: query cursor height + uInt32 width = std::min(myWidth, myFont.getStringWidth(text) + myTextXOfs * 2); + // Note: These include HiDPI scaling: + const Common::Rect imageRect = myDialog.instance().frameBuffer().imageRect(); + const Common::Rect dialogRect = myDialog.surface().dstRect(); + // Limit to app or screen size and adjust position + const Int32 xAbs = myPos.x + dialogRect.x() / myScale; + const uInt32 yAbs = myPos.y + dialogRect.y() / myScale; + Int32 x = std::min(xAbs, Int32(imageRect.w() / myScale - width)); + const uInt32 y = (yAbs + myHeight + hCursor > imageRect.h() / myScale) + ? yAbs - myHeight - VGAP + : yAbs + hCursor / myScale + VGAP; + + if(x < 0) + { + x = 0; + width = imageRect.w() / myScale; + } myWidget = widget; - // TODO: limit to app or screen size mySurface->setSrcSize(width, myHeight); - mySurface->setDstSize(width, myHeight); - mySurface->setDstPos(myPos.x, myPos.y + yOffset); + mySurface->setDstSize(width * myScale, myHeight * myScale); + mySurface->setDstPos(x * myScale, y * myScale); mySurface->frameRect(0, 0, width, myHeight, kColor); mySurface->fillRect(1, 1, width - 2, myHeight - 2, kWidColor); diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx index 7ba4dc044..d3164a09d 100644 --- a/src/gui/ToolTip.hxx +++ b/src/gui/ToolTip.hxx @@ -25,8 +25,9 @@ */ class OSystem; -class FBSurface; +class Dialog; class Widget; +class FBSurface; #include "Rect.hxx" @@ -34,7 +35,7 @@ class ToolTip { public: // Maximum tooltip length - static constexpr int MAX_LEN = 60; + static constexpr uInt32 MAX_LEN = 80; ToolTip(OSystem& instance, Dialog& dialog, const GUI::Font& font); ~ToolTip() = default; @@ -64,15 +65,16 @@ private: static constexpr uInt32 DELAY_TIME = 45; // display delay static constexpr int TEXT_Y_OFS = 2; - const GUI::Font& myFont; Dialog& myDialog; + const GUI::Font& myFont; Widget* myWidget{nullptr}; uInt32 myTimer{0}; Common::Point myPos; - int myWidth{0}; - int myHeight{0}; - int myTextXOfs{0}; + uInt32 myWidth{0}; + uInt32 myHeight{0}; + uInt32 myScale{1}; + uInt32 myTextXOfs{0}; shared_ptr mySurface; }; diff --git a/src/gui/VideoAudioDialog.cxx b/src/gui/VideoAudioDialog.cxx index 86db273b9..8d2c3be6d 100644 --- a/src/gui/VideoAudioDialog.cxx +++ b/src/gui/VideoAudioDialog.cxx @@ -122,6 +122,7 @@ void VideoAudioDialog::addDisplayTab() myRenderer = new PopUpWidget(myTab, _font, xpos, ypos, pwidth, lineHeight, instance().frameBuffer().supportedRenderers(), "Renderer ", lwidth); + myRenderer->setToolTip("Select renderer used for displaying screen."); wid.push_back(myRenderer); const int swidth = myRenderer->getWidth() - lwidth; ypos += lineHeight + VGAP; From c6068104d937a573abd7269decc318c7268c438b Mon Sep 17 00:00:00 2001 From: thrust26 Date: Mon, 16 Nov 2020 18:59:01 +0100 Subject: [PATCH 048/107] added a separate flag for mouse focus --- src/debugger/gui/DataGridWidget.cxx | 14 --------- src/debugger/gui/DataGridWidget.hxx | 2 -- src/debugger/gui/RomListWidget.cxx | 14 --------- src/debugger/gui/RomListWidget.hxx | 2 -- src/debugger/gui/TiaZoomWidget.cxx | 10 +----- src/debugger/gui/TiaZoomWidget.hxx | 3 -- src/debugger/gui/ToggleWidget.cxx | 14 --------- src/debugger/gui/ToggleWidget.hxx | 2 -- src/gui/CheckListWidget.cxx | 14 --------- src/gui/CheckListWidget.hxx | 4 --- src/gui/EditTextWidget.cxx | 14 --------- src/gui/EditTextWidget.hxx | 2 -- src/gui/GuiObject.hxx | 3 +- src/gui/PopUpWidget.cxx | 14 --------- src/gui/PopUpWidget.hxx | 2 -- src/gui/ScrollBarWidget.cxx | 10 +----- src/gui/ScrollBarWidget.hxx | 1 - src/gui/StringListWidget.cxx | 14 --------- src/gui/StringListWidget.hxx | 2 -- src/gui/TabWidget.cxx | 14 --------- src/gui/TabWidget.hxx | 4 +-- src/gui/Widget.cxx | 48 +++++++++-------------------- src/gui/Widget.hxx | 9 ++---- 23 files changed, 24 insertions(+), 192 deletions(-) diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index 877d96c6a..63c637ecd 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -240,20 +240,6 @@ void DataGridWidget::setRange(int lower, int upper) _upperBound = std::min(1 << _bits, upper); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void DataGridWidget::handleMouseEntered() -{ - if(isEnabled()) - setFlags(Widget::FLAG_HILITED); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void DataGridWidget::handleMouseLeft() -{ - if(isEnabled()) - clearFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DataGridWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) { diff --git a/src/debugger/gui/DataGridWidget.hxx b/src/debugger/gui/DataGridWidget.hxx index 82eca7fbc..9096879f3 100644 --- a/src/debugger/gui/DataGridWidget.hxx +++ b/src/debugger/gui/DataGridWidget.hxx @@ -101,8 +101,6 @@ class DataGridWidget : public EditableWidget void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; void handleMouseWheel(int x, int y, int direction) override; - void handleMouseEntered() override; - void handleMouseLeft() override; bool handleText(char text) override; bool handleKeyDown(StellaKey key, StellaMod mod) override; bool handleKeyUp(StellaKey key, StellaMod mod) override; diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 473df388e..58d392ee9 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -284,20 +284,6 @@ void RomListWidget::handleMouseWheel(int x, int y, int direction) myScrollBar->handleMouseWheel(x, y, direction); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void RomListWidget::handleMouseEntered() -{ - if(isEnabled()) - setFlags(Widget::FLAG_HILITED); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void RomListWidget::handleMouseLeft() -{ - if(isEnabled()) - clearFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool RomListWidget::handleText(char text) { diff --git a/src/debugger/gui/RomListWidget.hxx b/src/debugger/gui/RomListWidget.hxx index ec557f606..37473d12f 100644 --- a/src/debugger/gui/RomListWidget.hxx +++ b/src/debugger/gui/RomListWidget.hxx @@ -60,8 +60,6 @@ class RomListWidget : public EditableWidget void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; void handleMouseWheel(int x, int y, int direction) override; - void handleMouseEntered() override; - void handleMouseLeft() override; bool handleText(char text) override; bool handleKeyDown(StellaKey key, StellaMod mod) override; bool handleKeyUp(StellaKey key, StellaMod mod) override; diff --git a/src/debugger/gui/TiaZoomWidget.cxx b/src/debugger/gui/TiaZoomWidget.cxx index 759e95516..0895c51a8 100644 --- a/src/debugger/gui/TiaZoomWidget.cxx +++ b/src/debugger/gui/TiaZoomWidget.cxx @@ -178,19 +178,11 @@ void TiaZoomWidget::handleMouseMoved(int x, int y) } } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TiaZoomWidget::handleMouseEntered() -{ - if(isEnabled()) - setFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TiaZoomWidget::handleMouseLeft() { - if(isEnabled()) - clearFlags(Widget::FLAG_HILITED); myMouseMoving = false; + Widget::handleMouseLeft(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/debugger/gui/TiaZoomWidget.hxx b/src/debugger/gui/TiaZoomWidget.hxx index d4d3a9835..7169645bf 100644 --- a/src/debugger/gui/TiaZoomWidget.hxx +++ b/src/debugger/gui/TiaZoomWidget.hxx @@ -34,9 +34,6 @@ class TiaZoomWidget : public Widget, public CommandSender void loadConfig() override; void setPos(int x, int y); - protected: - void handleMouseEntered() override; - private: void zoom(int level); void recalc(); diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index ea3253d8a..4f9ddccfa 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -40,20 +40,6 @@ ToggleWidget::ToggleWidget(GuiObject* boss, const GUI::Font& font, Widget::FLAG_WANTS_RAWDATA; } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ToggleWidget::handleMouseEntered() -{ - if(isEnabled()) - setFlags(Widget::FLAG_HILITED); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ToggleWidget::handleMouseLeft() -{ - if(isEnabled()) - clearFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToggleWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) { diff --git a/src/debugger/gui/ToggleWidget.hxx b/src/debugger/gui/ToggleWidget.hxx index 67777a13a..0d6759467 100644 --- a/src/debugger/gui/ToggleWidget.hxx +++ b/src/debugger/gui/ToggleWidget.hxx @@ -68,8 +68,6 @@ class ToggleWidget : public Widget, public CommandSender void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; - void handleMouseEntered() override; - void handleMouseLeft() override; bool handleKeyDown(StellaKey key, StellaMod mod) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override; diff --git a/src/gui/CheckListWidget.cxx b/src/gui/CheckListWidget.cxx index 079989376..6d80fcc07 100644 --- a/src/gui/CheckListWidget.cxx +++ b/src/gui/CheckListWidget.cxx @@ -46,20 +46,6 @@ CheckListWidget::CheckListWidget(GuiObject* boss, const GUI::Font& font, } } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void CheckListWidget::handleMouseEntered() -{ - setFlags(Widget::FLAG_HILITED); - setDirty(); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void CheckListWidget::handleMouseLeft() -{ - clearFlags(Widget::FLAG_HILITED); - setDirty(); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void CheckListWidget::setList(const StringList& list, const BoolArray& state) { diff --git a/src/gui/CheckListWidget.hxx b/src/gui/CheckListWidget.hxx index 625875a9d..2ebec0d5d 100644 --- a/src/gui/CheckListWidget.hxx +++ b/src/gui/CheckListWidget.hxx @@ -42,10 +42,6 @@ class CheckListWidget : public ListWidget bool getState(int line); bool getSelectedState() { return getState(_selectedItem); } - protected: - void handleMouseEntered() override; - void handleMouseLeft() override; - private: bool handleEvent(Event::Type e) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override; diff --git a/src/gui/EditTextWidget.cxx b/src/gui/EditTextWidget.cxx index d3e4587a5..61785bd78 100644 --- a/src/gui/EditTextWidget.cxx +++ b/src/gui/EditTextWidget.cxx @@ -49,20 +49,6 @@ void EditTextWidget::setText(const string& str, bool changed) _changed = changed; } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void EditTextWidget::handleMouseEntered() -{ - if(isEnabled() && isEditable()) - setFlags(Widget::FLAG_HILITED); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void EditTextWidget::handleMouseLeft() -{ - if(isEnabled() && isEditable()) - clearFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void EditTextWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) { diff --git a/src/gui/EditTextWidget.hxx b/src/gui/EditTextWidget.hxx index fe063e042..e7c21beff 100644 --- a/src/gui/EditTextWidget.hxx +++ b/src/gui/EditTextWidget.hxx @@ -54,8 +54,6 @@ class EditTextWidget : public EditableWidget Common::Rect getEditRect() const override; void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; - void handleMouseEntered() override; - void handleMouseLeft() override; protected: string _backupString; diff --git a/src/gui/GuiObject.hxx b/src/gui/GuiObject.hxx index c276698f8..490df4344 100644 --- a/src/gui/GuiObject.hxx +++ b/src/gui/GuiObject.hxx @@ -52,7 +52,8 @@ class GuiObject : public CommandReceiver FLAG_RETAIN_FOCUS = 1 << 6, FLAG_WANTS_TAB = 1 << 7, FLAG_WANTS_RAWDATA = 1 << 8, - FLAG_NOBG = 1 << 9 + FLAG_NOBG = 1 << 9, + FLAG_MOUSE_FOCUS = 1 << 10 }; public: diff --git a/src/gui/PopUpWidget.cxx b/src/gui/PopUpWidget.cxx index ccf6af1aa..06c726779 100644 --- a/src/gui/PopUpWidget.cxx +++ b/src/gui/PopUpWidget.cxx @@ -158,20 +158,6 @@ void PopUpWidget::handleMouseWheel(int x, int y, int direction) } } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void PopUpWidget::handleMouseEntered() -{ - if(isEnabled()) - setFlags(Widget::FLAG_HILITED); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void PopUpWidget::handleMouseLeft() -{ - if(isEnabled()) - clearFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool PopUpWidget::handleEvent(Event::Type e) { diff --git a/src/gui/PopUpWidget.hxx b/src/gui/PopUpWidget.hxx index deb0af30c..babe28686 100644 --- a/src/gui/PopUpWidget.hxx +++ b/src/gui/PopUpWidget.hxx @@ -70,8 +70,6 @@ class PopUpWidget : public EditableWidget protected: void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleMouseWheel(int x, int y, int direction) override; - void handleMouseEntered() override; - void handleMouseLeft() override; bool handleEvent(Event::Type e) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override; diff --git a/src/gui/ScrollBarWidget.cxx b/src/gui/ScrollBarWidget.cxx index 04a99c732..73e89bc78 100644 --- a/src/gui/ScrollBarWidget.cxx +++ b/src/gui/ScrollBarWidget.cxx @@ -240,19 +240,11 @@ void ScrollBarWidget::checkBounds(int old_pos) } } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ScrollBarWidget::handleMouseEntered() -{ - if(isEnabled()) - setFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ScrollBarWidget::handleMouseLeft() { _part = Part::None; - if(isEnabled()) - clearFlags(Widget::FLAG_HILITED); + Widget::handleMouseLeft(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/ScrollBarWidget.hxx b/src/gui/ScrollBarWidget.hxx index 1d1b4c34a..0c29fde17 100644 --- a/src/gui/ScrollBarWidget.hxx +++ b/src/gui/ScrollBarWidget.hxx @@ -49,7 +49,6 @@ class ScrollBarWidget : public Widget, public CommandSender void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; void handleMouseMoved(int x, int y) override; bool handleMouseClicks(int x, int y, MouseButton b) override; - void handleMouseEntered() override; void handleMouseLeft() override; void setArrows(); diff --git a/src/gui/StringListWidget.cxx b/src/gui/StringListWidget.cxx index 5032654e6..ad5f1e7ec 100644 --- a/src/gui/StringListWidget.cxx +++ b/src/gui/StringListWidget.cxx @@ -50,20 +50,6 @@ void StringListWidget::setList(const StringList& list) ListWidget::recalc(); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void StringListWidget::handleMouseEntered() -{ - if(isEnabled()) - setFlags(Widget::FLAG_HILITED); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void StringListWidget::handleMouseLeft() -{ - if(isEnabled()) - clearFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StringListWidget::drawWidget(bool hilite) { diff --git a/src/gui/StringListWidget.hxx b/src/gui/StringListWidget.hxx index ee3b56e97..ed5874b91 100644 --- a/src/gui/StringListWidget.hxx +++ b/src/gui/StringListWidget.hxx @@ -33,8 +33,6 @@ class StringListWidget : public ListWidget bool wantsFocus() const override { return true; } protected: - void handleMouseEntered() override; - void handleMouseLeft() override; // display depends on _hasFocus so we have to redraw when focus changes void receivedFocusWidget() override { setDirty(); } void lostFocusWidget() override { setDirty(); } diff --git a/src/gui/TabWidget.cxx b/src/gui/TabWidget.cxx index 5e96338ab..28b383f6c 100644 --- a/src/gui/TabWidget.cxx +++ b/src/gui/TabWidget.cxx @@ -213,20 +213,6 @@ void TabWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) } } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TabWidget::handleMouseEntered() -{ - //if(isEnabled()) - // setFlags(Widget::FLAG_HILITED); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TabWidget::handleMouseLeft() -{ - //if(isEnabled()) - // clearFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TabWidget::handleCommand(CommandSender* sender, int cmd, int data, int id) { diff --git a/src/gui/TabWidget.hxx b/src/gui/TabWidget.hxx index 1f581df5c..141b29497 100644 --- a/src/gui/TabWidget.hxx +++ b/src/gui/TabWidget.hxx @@ -63,8 +63,8 @@ class TabWidget : public Widget, public CommandSender protected: void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; - void handleMouseEntered() override; - void handleMouseLeft() override; + void handleMouseEntered() override {} + void handleMouseLeft() override {} void handleCommand(CommandSender* sender, int cmd, int data, int id) override; bool handleEvent(Event::Type event) override; diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index f37f8840c..991207655 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -75,12 +75,8 @@ void Widget::tick() { if(isEnabled()) { - if(isHighlighted() && hasToolTip()) + if(hasMouseFocus() && hasToolTip()) dialog().tooltip().request(this); - //{ - // if(dialog().enableToolTip()) - // dialog().showToolTip(10, 10); - //} // Recursively tick widget and all child dialogs and widgets Widget* w = _firstWidget; @@ -181,6 +177,20 @@ void Widget::drawChain() } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::handleMouseEntered() +{ + if(isEnabled()) + setFlags(Widget::FLAG_HILITED | Widget::FLAG_MOUSE_FOCUS); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::handleMouseLeft() +{ + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED | Widget::FLAG_MOUSE_FOCUS); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Widget::receivedFocus() { @@ -465,20 +475,6 @@ ButtonWidget::ButtonWidget(GuiObject* boss, const GUI::Font& font, _bmh = bmh; } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ButtonWidget::handleMouseEntered() -{ - if(isEnabled()) - setFlags(Widget::FLAG_HILITED); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ButtonWidget::handleMouseLeft() -{ - if(isEnabled()) - clearFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool ButtonWidget::handleEvent(Event::Type e) { @@ -576,20 +572,6 @@ CheckboxWidget::CheckboxWidget(GuiObject* boss, const GUI::Font& font, setFill(CheckboxWidget::FillType::Normal); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void CheckboxWidget::handleMouseEntered() -{ - if(isEnabled()) - setFlags(Widget::FLAG_HILITED); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void CheckboxWidget::handleMouseLeft() -{ - if(isEnabled()) - clearFlags(Widget::FLAG_HILITED); -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void CheckboxWidget::handleMouseUp(int x, int y, MouseButton b, int clickCount) { diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 98e36863f..bf831a5eb 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -58,8 +58,8 @@ class Widget : public GuiObject virtual bool handleKeyUp(StellaKey key, StellaMod mod) { return false; } virtual void handleMouseDown(int x, int y, MouseButton b, int clickCount) { } virtual void handleMouseUp(int x, int y, MouseButton b, int clickCount) { } - virtual void handleMouseEntered() { } - virtual void handleMouseLeft() { } + virtual void handleMouseEntered(); + virtual void handleMouseLeft(); virtual void handleMouseMoved(int x, int y) { } virtual void handleMouseWheel(int x, int y, int direction) { } virtual bool handleMouseClicks(int x, int y, MouseButton b) { return false; } @@ -88,6 +88,7 @@ class Widget : public GuiObject bool isEnabled() const { return _flags & FLAG_ENABLED; } bool isVisible() const override { return !(_flags & FLAG_INVISIBLE); } bool isHighlighted() const { return _flags & FLAG_HILITED; } + bool hasMouseFocus() const { return _flags & FLAG_MOUSE_FOCUS; } virtual bool wantsFocus() const { return _flags & FLAG_RETAIN_FOCUS; } bool wantsTab() const { return _flags & FLAG_WANTS_TAB; } bool wantsRaw() const { return _flags & FLAG_WANTS_RAWDATA; } @@ -231,8 +232,6 @@ class ButtonWidget : public StaticTextWidget, public CommandSender bool handleMouseClicks(int x, int y, MouseButton b) override; void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; - void handleMouseEntered() override; - void handleMouseLeft() override; bool handleEvent(Event::Type event) override; void drawWidget(bool hilite) override; @@ -273,8 +272,6 @@ class CheckboxWidget : public ButtonWidget bool getState() const { return _state; } void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; - void handleMouseEntered() override; - void handleMouseLeft() override; static int boxSize(const GUI::Font& font) { From 004b34f51e5e61305cec92f90f335df73ce292b3 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Mon, 16 Nov 2020 20:00:51 +0100 Subject: [PATCH 049/107] fixed tool tip font for Launcher added a few more tool tips --- src/gui/LauncherDialog.cxx | 3 ++- src/gui/ToolTip.cxx | 5 +++-- src/gui/VideoAudioDialog.cxx | 5 +++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index abae25742..93361b9f5 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -56,7 +56,8 @@ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, int x, int y, int w, int h) - : Dialog(osystem, parent, x, y, w, h) + : Dialog(osystem, parent, osystem.frameBuffer().launcherFont(), "", + x, y, w, h) { myUseMinimalUI = instance().settings().getBool("minimal_ui"); diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 30100009a..82c0df10b 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -48,8 +48,10 @@ void ToolTip::request(Widget* widget) } if(myTimer == DELAY_TIME) { + myWidget = widget; + const uInt32 VGAP = 1; - const uInt32 hCursor = 19; // TODO: query cursor height + const uInt32 hCursor = 18; string text = widget->getToolTip(); uInt32 width = std::min(myWidth, myFont.getStringWidth(text) + myTextXOfs * 2); // Note: These include HiDPI scaling: @@ -69,7 +71,6 @@ void ToolTip::request(Widget* widget) width = imageRect.w() / myScale; } - myWidget = widget; mySurface->setSrcSize(width, myHeight); mySurface->setDstSize(width * myScale, myHeight * myScale); mySurface->setDstPos(x * myScale, y * myScale); diff --git a/src/gui/VideoAudioDialog.cxx b/src/gui/VideoAudioDialog.cxx index 8d2c3be6d..d34f0a330 100644 --- a/src/gui/VideoAudioDialog.cxx +++ b/src/gui/VideoAudioDialog.cxx @@ -129,6 +129,7 @@ void VideoAudioDialog::addDisplayTab() // TIA interpolation myTIAInterpolate = new CheckboxWidget(myTab, _font, xpos, ypos + 1, "Interpolation "); + myTIAInterpolate->setToolTip("Blur the emulated display."); wid.push_back(myTIAInterpolate); ypos += lineHeight + VGAP * 4; @@ -146,12 +147,14 @@ void VideoAudioDialog::addDisplayTab() // FS stretch myUseStretch = new CheckboxWidget(myTab, _font, xpos + INDENT, ypos + 1, "Stretch"); + myUseStretch->setToolTip("Stretch the emulated display to fill the whole screen."); wid.push_back(myUseStretch); #ifdef ADAPTABLE_REFRESH_SUPPORT // Adapt refresh rate ypos += lineHeight + VGAP; myRefreshAdapt = new CheckboxWidget(myTab, _font, xpos + INDENT, ypos + 1, "Adapt display refresh rate"); + myRefreshAdapt->setToolTip("Select the optimal display refresh rate for each ROM."); wid.push_back(myRefreshAdapt); #else myRefreshAdapt = nullptr; @@ -168,6 +171,7 @@ void VideoAudioDialog::addDisplayTab() // Aspect ratio correction ypos += lineHeight + VGAP * 4; myCorrectAspect = new CheckboxWidget(myTab, _font, xpos, ypos + 1, "Correct aspect ratio (*)"); + myCorrectAspect->setToolTip("Uncheck to disable real world aspect ratio correction."); wid.push_back(myCorrectAspect); // Vertical size @@ -177,6 +181,7 @@ void VideoAudioDialog::addDisplayTab() "V-Size adjust", lwidth, kVSizeChanged, fontWidth * 7, "%", 0, true); myVSizeAdjust->setMinValue(-5); myVSizeAdjust->setMaxValue(5); myVSizeAdjust->setTickmarkIntervals(2); + myVSizeAdjust->setToolTip("Adapt vertical size to emulated TV display."); wid.push_back(myVSizeAdjust); From 99c0cd66bc94648b1a2ee741739a29cd127d3081 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Mon, 16 Nov 2020 23:50:10 +0100 Subject: [PATCH 050/107] added value tool tips to debugger (DataGridWiget, ToogleWidget) --- src/debugger/gui/CpuWidget.cxx | 4 ++-- src/debugger/gui/DataGridWidget.cxx | 17 +++++++++++++++++ src/debugger/gui/DataGridWidget.hxx | 3 +++ src/debugger/gui/ToggleWidget.cxx | 26 ++++++++++++++++++++++++++ src/debugger/gui/ToggleWidget.hxx | 3 +++ src/gui/Dialog.cxx | 2 +- src/gui/Dialog.hxx | 3 --- src/gui/ToolTip.cxx | 29 +++++++++++++++++++---------- src/gui/ToolTip.hxx | 11 ++++++----- src/gui/Widget.hxx | 8 ++++++-- 10 files changed, 83 insertions(+), 23 deletions(-) diff --git a/src/debugger/gui/CpuWidget.cxx b/src/debugger/gui/CpuWidget.cxx index 31f939f01..ea6fbb96f 100644 --- a/src/debugger/gui/CpuWidget.cxx +++ b/src/debugger/gui/CpuWidget.cxx @@ -109,10 +109,10 @@ CpuWidget::CpuWidget(GuiObject* boss, const GUI::Font& lfont, const GUI::Font& n { new StaticTextWidget(boss, lfont, myCpuGridDecValue->getLeft() - fontWidth, ypos + row * lineHeight + 2, - lwidth - 2, fontHeight, "#"); + fontWidth, fontHeight, "#"); new StaticTextWidget(boss, lfont, myCpuGridBinValue->getLeft() - fontWidth, ypos + row * lineHeight + 2, - lwidth - 2, fontHeight, "%"); + fontWidth, fontHeight, "%"); } // Create a bitfield widget for changing the processor status diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index 63c637ecd..f6898b6c8 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -570,6 +570,23 @@ void DataGridWidget::handleCommand(CommandSender* sender, int cmd, } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string DataGridWidget::getToolTip(int x, int y) const +{ + const int col = (x - getAbsX()) / _colWidth; + const int row = (y - getAbsY()) / _rowHeight; + const int pos = row * _cols + col; + const Int32 val = _valueList[pos]; + const string hex = Common::Base::toString(val, Common::Base::Fmt::_16); + const string dec = Common::Base::toString(val, Common::Base::Fmt::_10); + const string bin = Common::Base::toString(val, Common::Base::Fmt::_2); + ostringstream buf; + + // TODO: time leading spaces and zeroes + buf << "$" << hex << " = #" << dec << " = %" << bin; + return buf.str(); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DataGridWidget::drawWidget(bool hilite) { diff --git a/src/debugger/gui/DataGridWidget.hxx b/src/debugger/gui/DataGridWidget.hxx index 9096879f3..f50270f4f 100644 --- a/src/debugger/gui/DataGridWidget.hxx +++ b/src/debugger/gui/DataGridWidget.hxx @@ -84,6 +84,9 @@ class DataGridWidget : public EditableWidget void setCrossed(bool enable) { _crossGrid = enable; } + string getToolTip(int x = 0, int y = 0) const override; + bool hasToolTip() const override { return true; } + protected: void drawWidget(bool hilite) override; diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index 4f9ddccfa..346c51121 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -16,6 +16,7 @@ //============================================================================ #include "OSystem.hxx" +#include "Base.hxx" #include "StellaKeys.hxx" #include "Widget.hxx" #include "ToggleWidget.hxx" @@ -209,3 +210,28 @@ void ToggleWidget::handleCommand(CommandSender* sender, int cmd, } } } + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string ToggleWidget::getToolTip(int x, int y) const +{ + const int row = (y - getAbsY()) / _rowHeight; + const int pos = row * _cols;// +col; + Int32 val = 0; + + for(int col = 0; col < _cols; ++col) + { + val <<= 1; + val += _stateList[pos + col]; + } + + const string hex = Common::Base::toString(val, Common::Base::Fmt::_16); + const string dec = Common::Base::toString(val, Common::Base::Fmt::_10); + const string bin = Common::Base::toString(val, Common::Base::Fmt::_2); + ostringstream buf; + + // TODO: time leading spaces and zeroes + buf << "$" << hex << " = #" << dec << " = %" << bin; + return buf.str(); +} + + diff --git a/src/debugger/gui/ToggleWidget.hxx b/src/debugger/gui/ToggleWidget.hxx index 0d6759467..a671f53e9 100644 --- a/src/debugger/gui/ToggleWidget.hxx +++ b/src/debugger/gui/ToggleWidget.hxx @@ -46,6 +46,9 @@ class ToggleWidget : public Widget, public CommandSender void setEditable(bool editable) { _editable = editable; } bool isEditable() const { return _editable; } + string getToolTip(int x = 0, int y = 0) const override; + bool hasToolTip() const override { return true; } + protected: protected: diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index f58d98536..b9678182e 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -67,7 +67,7 @@ Dialog::Dialog(OSystem& instance, DialogContainer& parent, const GUI::Font& font attr.blendalpha = 25; // darken background dialogs by 25% _shadeSurface->applyAttributes(); - _toolTip = make_unique(instance, *this, font); + _toolTip = make_unique(*this, font); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index b134d7d37..f628ec792 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -124,9 +124,6 @@ class Dialog : public GuiObject bool shouldResize(uInt32& w, uInt32& h) const; ToolTip& tooltip() { return *_toolTip; } - //bool enableToolTip(); - //void showToolTip(int x, int y); - //void hideToolTip(); protected: void draw() override { } diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 82c0df10b..9531ee5ef 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -24,35 +24,44 @@ #include "ToolTip.hxx" +// TODO: +// - disable when in edit mode +// - option to disable tips +// - multi line tips +// - nicer formating of DataDridWidget tip +// - allow reversing ToogleWidget (TooglePixelWidget) + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -ToolTip::ToolTip(OSystem& instance, Dialog& dialog, const GUI::Font& font) +ToolTip::ToolTip(Dialog& dialog, const GUI::Font& font) : myDialog(dialog), myFont(font) { const int fontWidth = font.getMaxCharWidth(), fontHeight = font.getFontHeight(); - myTextXOfs = fontHeight < 24 ? 5 : 8; //3 : 5; - myWidth = myTextXOfs * 2 + fontWidth * MAX_LEN; - myHeight = fontHeight + TEXT_Y_OFS * 2; + myTextXOfs = fontHeight < 24 ? 5 : 8; // 3 : 5; + myTextYOfs = fontHeight < 24 ? 2 : 3; + myWidth = fontWidth * MAX_LEN + myTextXOfs * 2; + myHeight = fontHeight + myTextYOfs * 2; + mySurface = myDialog.instance().frameBuffer().allocateSurface(myWidth, myHeight); myScale = myDialog.instance().frameBuffer().hidpiScaleFactor(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ToolTip::request(Widget* widget) +void ToolTip::request(const Widget* widget) { if(myWidget != widget) - { release(); - } + if(myTimer == DELAY_TIME) { myWidget = widget; const uInt32 VGAP = 1; const uInt32 hCursor = 18; - string text = widget->getToolTip(); + string text = widget->getToolTip(myPos.x, myPos.y); uInt32 width = std::min(myWidth, myFont.getStringWidth(text) + myTextXOfs * 2); // Note: These include HiDPI scaling: const Common::Rect imageRect = myDialog.instance().frameBuffer().imageRect(); @@ -77,8 +86,8 @@ void ToolTip::request(Widget* widget) mySurface->frameRect(0, 0, width, myHeight, kColor); mySurface->fillRect(1, 1, width - 2, myHeight - 2, kWidColor); - mySurface->drawString(myFont, text, myTextXOfs, TEXT_Y_OFS, - width - myTextXOfs * 2, myHeight - TEXT_Y_OFS * 2, kTextColor); + mySurface->drawString(myFont, text, myTextXOfs, myTextYOfs, + width - myTextXOfs * 2, myHeight - myTextYOfs * 2, kTextColor); myDialog.setDirtyChain(); } myTimer++; diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx index d3164a09d..125343294 100644 --- a/src/gui/ToolTip.hxx +++ b/src/gui/ToolTip.hxx @@ -19,7 +19,7 @@ #define TOOL_TIP_HXX /** - * Class for providing tooltip functionality + * Class for providing tool tip functionality * * @author Thomas Jentzsch */ @@ -37,13 +37,13 @@ public: // Maximum tooltip length static constexpr uInt32 MAX_LEN = 80; - ToolTip(OSystem& instance, Dialog& dialog, const GUI::Font& font); + ToolTip(Dialog& dialog, const GUI::Font& font); ~ToolTip() = default; /** Request a tooltip display */ - void request(Widget* widget); + void request(const Widget* widget); /** @@ -63,18 +63,19 @@ public: private: static constexpr uInt32 DELAY_TIME = 45; // display delay - static constexpr int TEXT_Y_OFS = 2; + //static constexpr int TEXT_Y_OFS = 2; Dialog& myDialog; const GUI::Font& myFont; - Widget* myWidget{nullptr}; + const Widget* myWidget{nullptr}; uInt32 myTimer{0}; Common::Point myPos; uInt32 myWidth{0}; uInt32 myHeight{0}; uInt32 myScale{1}; uInt32 myTextXOfs{0}; + uInt32 myTextYOfs{0}; shared_ptr mySurface; }; diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index bf831a5eb..42612cc99 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -105,8 +105,8 @@ class Widget : public GuiObject void setShadowColor(ColorId color) { _shadowcolor = color; setDirty(); } void setToolTip(const string& text); - const string& getToolTip() const { return _toolTipText; } - bool hasToolTip() const { return !_toolTipText.empty(); } + virtual string getToolTip(int x = 0, int y = 0) const { return _toolTipText; } + virtual bool hasToolTip() const { return !_toolTipText.empty(); } virtual void loadConfig() { } @@ -181,6 +181,10 @@ class StaticTextWidget : public Widget const string& text = "", TextAlign align = TextAlign::Left, ColorId shadowColor = kNone); ~StaticTextWidget() override = default; + + void handleMouseEntered() override {} + void handleMouseLeft() override {} + void setValue(int value); void setLabel(const string& label); void setAlign(TextAlign align) { _align = align; setDirty(); } From f1f5938b79d76eef7df5cd769b15f28516c3006d Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 17 Nov 2020 08:34:39 +0100 Subject: [PATCH 051/107] fixed bug which removed highlighting for most widgets --- src/debugger/gui/ToggleWidget.cxx | 2 +- src/gui/ToolTip.cxx | 3 ++- src/gui/ToolTip.hxx | 2 +- src/gui/Widget.cxx | 14 ++++++++++++++ src/gui/Widget.hxx | 2 ++ 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index 346c51121..a73bc25dc 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -212,7 +212,7 @@ void ToggleWidget::handleCommand(CommandSender* sender, int cmd, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -string ToggleWidget::getToolTip(int x, int y) const +string ToggleWidget::getToolTip(int, int y) const { const int row = (y - getAbsY()) / _rowHeight; const int pos = row * _cols;// +col; diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 9531ee5ef..ece2f1ccc 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -30,7 +30,8 @@ // - multi line tips // - nicer formating of DataDridWidget tip // - allow reversing ToogleWidget (TooglePixelWidget) - +// - shift checkbox bits +// - RomListWidget (hex codes, maybe disassembly operands) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ToolTip::ToolTip(Dialog& dialog, const GUI::Font& font) diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx index 125343294..efcf3bafe 100644 --- a/src/gui/ToolTip.hxx +++ b/src/gui/ToolTip.hxx @@ -19,7 +19,7 @@ #define TOOL_TIP_HXX /** - * Class for providing tool tip functionality + * Class for providing tooltip functionality * * @author Thomas Jentzsch */ diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 991207655..a7fb55561 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -475,6 +475,20 @@ ButtonWidget::ButtonWidget(GuiObject* boss, const GUI::Font& font, _bmh = bmh; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ButtonWidget::handleMouseEntered() +{ + if(isEnabled()) + setFlags(Widget::FLAG_HILITED | Widget::FLAG_MOUSE_FOCUS); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ButtonWidget::handleMouseLeft() +{ + if(isEnabled()) + clearFlags(Widget::FLAG_HILITED | Widget::FLAG_MOUSE_FOCUS); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool ButtonWidget::handleEvent(Event::Type e) { diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 42612cc99..33c6a3e20 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -236,6 +236,8 @@ class ButtonWidget : public StaticTextWidget, public CommandSender bool handleMouseClicks(int x, int y, MouseButton b) override; void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; + void handleMouseEntered() override; + void handleMouseLeft() override; bool handleEvent(Event::Type event) override; void drawWidget(bool hilite) override; From d7fe5510bb50c9cff4304d72c6a1c04d58d93e0f Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 17 Nov 2020 12:33:47 +0100 Subject: [PATCH 052/107] keep tooltips visible while mouse moves in focus show tooltips faster when moving from one to another update tooltip when mouse moves over different widget items disable tooltip when editing --- src/debugger/gui/DataGridWidget.cxx | 22 ++++- src/debugger/gui/DataGridWidget.hxx | 8 +- src/debugger/gui/ToggleWidget.cxx | 22 ++++- src/debugger/gui/ToggleWidget.hxx | 7 +- src/gui/Dialog.cxx | 6 +- src/gui/EditableWidget.cxx | 8 ++ src/gui/EditableWidget.hxx | 1 + src/gui/LauncherDialog.cxx | 2 +- src/gui/PopUpWidget.cxx | 3 + src/gui/ToolTip.cxx | 139 +++++++++++++++++----------- src/gui/ToolTip.hxx | 79 +++++++++------- src/gui/VideoAudioDialog.cxx | 8 +- src/gui/Widget.cxx | 4 +- src/gui/Widget.hxx | 8 +- 14 files changed, 205 insertions(+), 112 deletions(-) diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index f6898b6c8..d11375d29 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -571,12 +571,18 @@ void DataGridWidget::handleCommand(CommandSender* sender, int cmd, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -string DataGridWidget::getToolTip(int x, int y) const +int DataGridWidget::getToolTipIndex(Common::Point pos) const { - const int col = (x - getAbsX()) / _colWidth; - const int row = (y - getAbsY()) / _rowHeight; - const int pos = row * _cols + col; - const Int32 val = _valueList[pos]; + const int col = (pos.x - getAbsX()) / _colWidth; + const int row = (pos.y - getAbsY()) / _rowHeight; + + return row * _cols + col; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string DataGridWidget::getToolTip(Common::Point pos) const +{ + const Int32 val = _valueList[getToolTipIndex(pos)]; const string hex = Common::Base::toString(val, Common::Base::Fmt::_16); const string dec = Common::Base::toString(val, Common::Base::Fmt::_10); const string bin = Common::Base::toString(val, Common::Base::Fmt::_2); @@ -587,6 +593,12 @@ string DataGridWidget::getToolTip(int x, int y) const return buf.str(); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool DataGridWidget::changedToolTip(Common::Point oldPos, Common::Point newPos) const +{ + return getToolTipIndex(oldPos) != getToolTipIndex(newPos); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DataGridWidget::drawWidget(bool hilite) { diff --git a/src/debugger/gui/DataGridWidget.hxx b/src/debugger/gui/DataGridWidget.hxx index f50270f4f..1a402659d 100644 --- a/src/debugger/gui/DataGridWidget.hxx +++ b/src/debugger/gui/DataGridWidget.hxx @@ -84,8 +84,8 @@ class DataGridWidget : public EditableWidget void setCrossed(bool enable) { _crossGrid = enable; } - string getToolTip(int x = 0, int y = 0) const override; - bool hasToolTip() const override { return true; } + string getToolTip(Common::Point pos) const override; + bool changedToolTip(Common::Point oldPos, Common::Point newPos) const override; protected: void drawWidget(bool hilite) override; @@ -101,6 +101,8 @@ class DataGridWidget : public EditableWidget void receivedFocusWidget() override; void lostFocusWidget() override; + bool hasToolTip() const override { return true; } + void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; void handleMouseWheel(int x, int y, int direction) override; @@ -148,6 +150,8 @@ class DataGridWidget : public EditableWidget void enableEditMode(bool state) { _editMode = state; } + int getToolTipIndex(Common::Point pos) const; + private: // Following constructors and assignment operators not supported DataGridWidget() = delete; diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index a73bc25dc..3d2fca262 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -212,16 +212,23 @@ void ToggleWidget::handleCommand(CommandSender* sender, int cmd, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -string ToggleWidget::getToolTip(int, int y) const +int ToggleWidget::getToolTipIndex(Common::Point pos) const { - const int row = (y - getAbsY()) / _rowHeight; - const int pos = row * _cols;// +col; + const int row = (pos.y - getAbsY()) / _rowHeight; + + return row * _cols; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string ToggleWidget::getToolTip(Common::Point pos) const +{ + const int idx = getToolTipIndex(pos); Int32 val = 0; for(int col = 0; col < _cols; ++col) { val <<= 1; - val += _stateList[pos + col]; + val += _stateList[idx + col]; } const string hex = Common::Base::toString(val, Common::Base::Fmt::_16); @@ -234,4 +241,11 @@ string ToggleWidget::getToolTip(int, int y) const return buf.str(); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool ToggleWidget::changedToolTip(Common::Point oldPos, Common::Point newPos) const +{ + return getToolTipIndex(oldPos) != getToolTipIndex(newPos); +} + + diff --git a/src/debugger/gui/ToggleWidget.hxx b/src/debugger/gui/ToggleWidget.hxx index a671f53e9..060a52e13 100644 --- a/src/debugger/gui/ToggleWidget.hxx +++ b/src/debugger/gui/ToggleWidget.hxx @@ -46,10 +46,11 @@ class ToggleWidget : public Widget, public CommandSender void setEditable(bool editable) { _editable = editable; } bool isEditable() const { return _editable; } - string getToolTip(int x = 0, int y = 0) const override; - bool hasToolTip() const override { return true; } + string getToolTip(Common::Point pos) const override; + bool changedToolTip(Common::Point oldPos, Common::Point newPos) const override; protected: + bool hasToolTip() const override { return true; } protected: int _rows; @@ -74,6 +75,8 @@ class ToggleWidget : public Widget, public CommandSender bool handleKeyDown(StellaKey key, StellaMod mod) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override; + int getToolTipIndex(Common::Point pos) const; + // Following constructors and assignment operators not supported ToggleWidget() = delete; ToggleWidget(const ToggleWidget&) = delete; diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index b9678182e..a33b4ffa1 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -593,9 +593,6 @@ void Dialog::handleMouseMoved(int x, int y) { Widget* w; - // Update mouse coordinates for tooltips - _toolTip->update(x, y); - if(_focusedWidget && !_dragWidget) { w = _focusedWidget; @@ -639,6 +636,9 @@ void Dialog::handleMouseMoved(int x, int y) if (w && (w->getFlags() & Widget::FLAG_TRACK_MOUSE)) w->handleMouseMoved(x - (w->getAbsX() - _x), y - (w->getAbsY() - _y)); + + // Update mouse coordinates for tooltips + _toolTip->update(_mouseWidget, Common::Point(x, y)); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index d6c35fa55..39a4b519f 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -22,6 +22,7 @@ #include "OSystem.hxx" #include "EventHandler.hxx" #include "UndoHandler.hxx" +#include "ToolTip.hxx" #include "EditableWidget.hxx" // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -79,6 +80,12 @@ void EditableWidget::tick() Widget::tick(); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool EditableWidget::wantsToolTip() const +{ + return !(_hasFocus && isEditable() && _editMode) && Widget::wantsToolTip(); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void EditableWidget::setEditable(bool editable, bool hiliteBG) { @@ -100,6 +107,7 @@ void EditableWidget::receivedFocusWidget() { _caretTimer = 0; _caretEnabled = true; + dialog().tooltip().release(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/EditableWidget.hxx b/src/gui/EditableWidget.hxx index 8ebf1791c..6e55cf541 100644 --- a/src/gui/EditableWidget.hxx +++ b/src/gui/EditableWidget.hxx @@ -68,6 +68,7 @@ class EditableWidget : public Widget, public CommandSender void receivedFocusWidget() override; void lostFocusWidget() override; void tick() override; + bool wantsToolTip() const override; virtual void startEditMode() { setFlags(Widget::FLAG_WANTS_RAWDATA); } virtual void endEditMode() { clearFlags(Widget::FLAG_WANTS_RAWDATA); } diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 93361b9f5..9c140e15c 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -121,7 +121,7 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, // Show the filter input field xpos -= fwidth + LBL_GAP; myPattern = new EditTextWidget(this, font, xpos, ypos - 2, fwidth, lineHeight, ""); - myPattern->setToolTip("Enter a filter text to reduce file list."); + myPattern->setToolTip("Enter filter text to reduce file list."); // Show the "Filter" label xpos -= lwidth3 + LBL_GAP; new StaticTextWidget(this, font, xpos, ypos, lblFilter); diff --git a/src/gui/PopUpWidget.cxx b/src/gui/PopUpWidget.cxx index 06c726779..45f79e55b 100644 --- a/src/gui/PopUpWidget.cxx +++ b/src/gui/PopUpWidget.cxx @@ -20,6 +20,8 @@ #include "FBSurface.hxx" #include "Font.hxx" #include "ContextMenu.hxx" +#include "Dialog.hxx" +#include "ToolTip.hxx" #include "DialogContainer.hxx" #include "PopUpWidget.hxx" @@ -122,6 +124,7 @@ void PopUpWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) { if(isEnabled() && !myMenu->isVisible()) { + dialog().tooltip().hide(); // Add menu just underneath parent widget myMenu->show(getAbsX() + _labelWidth, getAbsY() + getHeight(), dialog().surface().dstRect(), myMenu->getSelected()); diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index ece2f1ccc..36c8b1b20 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -24,8 +24,12 @@ #include "ToolTip.hxx" -// TODO: -// - disable when in edit mode +// TODOs: +// + keep enabled when mouse moves over same widget +// + static position and text for normal widgets +// + moving position and text over e.g. DataGridWidget +// + reenable tip faster +// + disable when in edit mode // - option to disable tips // - multi line tips // - nicer formating of DataDridWidget tip @@ -41,7 +45,7 @@ ToolTip::ToolTip(Dialog& dialog, const GUI::Font& font) const int fontWidth = font.getMaxCharWidth(), fontHeight = font.getFontHeight(); - myTextXOfs = fontHeight < 24 ? 5 : 8; // 3 : 5; + myTextXOfs = fontHeight < 24 ? 5 : 8; myTextYOfs = fontHeight < 24 ? 2 : 3; myWidth = fontWidth * MAX_LEN + myTextXOfs * 2; myHeight = fontHeight + myTextYOfs * 2; @@ -51,72 +55,103 @@ ToolTip::ToolTip(Dialog& dialog, const GUI::Font& font) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ToolTip::request(const Widget* widget) +void ToolTip::update(const Widget* widget, Common::Point pos) { - if(myWidget != widget) - release(); - - if(myTimer == DELAY_TIME) + if(myTipWidget != widget) { - myWidget = widget; + myFocusWidget = widget; + release(); + } + if(myTipShown && myTipWidget->changedToolTip(myPos, pos)) + myPos = pos, show(); + else + myPos = pos; +} - const uInt32 VGAP = 1; - const uInt32 hCursor = 18; - string text = widget->getToolTip(myPos.x, myPos.y); - uInt32 width = std::min(myWidth, myFont.getStringWidth(text) + myTextXOfs * 2); - // Note: These include HiDPI scaling: - const Common::Rect imageRect = myDialog.instance().frameBuffer().imageRect(); - const Common::Rect dialogRect = myDialog.surface().dstRect(); - // Limit to app or screen size and adjust position - const Int32 xAbs = myPos.x + dialogRect.x() / myScale; - const uInt32 yAbs = myPos.y + dialogRect.y() / myScale; - Int32 x = std::min(xAbs, Int32(imageRect.w() / myScale - width)); - const uInt32 y = (yAbs + myHeight + hCursor > imageRect.h() / myScale) - ? yAbs - myHeight - VGAP - : yAbs + hCursor / myScale + VGAP; +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ToolTip::hide() +{ + if(myTipShown) + { + myTimer = 0; + myTipWidget = myFocusWidget = nullptr; - if(x < 0) - { - x = 0; - width = imageRect.w() / myScale; - } - - mySurface->setSrcSize(width, myHeight); - mySurface->setDstSize(width * myScale, myHeight * myScale); - mySurface->setDstPos(x * myScale, y * myScale); - - mySurface->frameRect(0, 0, width, myHeight, kColor); - mySurface->fillRect(1, 1, width - 2, myHeight - 2, kWidColor); - mySurface->drawString(myFont, text, myTextXOfs, myTextYOfs, - width - myTextXOfs * 2, myHeight - myTextYOfs * 2, kTextColor); + myTipShown = false; myDialog.setDirtyChain(); } - myTimer++; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToolTip::release() { - if(myWidget != nullptr) + if(myTipShown) { - myTimer = 0; - myWidget = nullptr; + myTimer = DELAY_TIME; + + myTipShown = false; myDialog.setDirtyChain(); } + + // After displaying a tip, slowly reset the timer to 0 + // until a new tip is requested + if(myTipWidget != myFocusWidget && myTimer) + myTimer--; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ToolTip::request() +{ + myTipWidget = myFocusWidget; + + if(myTimer == DELAY_TIME) + show(); + + myTimer++; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ToolTip::show() +{ + if(myTipWidget == nullptr) + return; + + const uInt32 V_GAP = 1; + const uInt32 H_CURSOR = 18; + string text = myTipWidget->getToolTip(myPos); + uInt32 width = std::min(myWidth, myFont.getStringWidth(text) + myTextXOfs * 2); + // Note: The rects include HiDPI scaling + const Common::Rect imageRect = myDialog.instance().frameBuffer().imageRect(); + const Common::Rect dialogRect = myDialog.surface().dstRect(); + // Limit position to app size and adjust accordingly + const Int32 xAbs = myPos.x + dialogRect.x() / myScale; + const uInt32 yAbs = myPos.y + dialogRect.y() / myScale; + Int32 x = std::min(xAbs, Int32(imageRect.w() / myScale - width)); + const uInt32 y = (yAbs + myHeight + H_CURSOR > imageRect.h() / myScale) + ? yAbs - myHeight - V_GAP + : yAbs + H_CURSOR / myScale + V_GAP; + + if(x < 0) + { + x = 0; + width = imageRect.w() / myScale; + } + + mySurface->setSrcSize(width, myHeight); + mySurface->setDstSize(width * myScale, myHeight * myScale); + mySurface->setDstPos(x * myScale, y * myScale); + + mySurface->frameRect(0, 0, width, myHeight, kColor); + mySurface->fillRect(1, 1, width - 2, myHeight - 2, kWidColor); + mySurface->drawString(myFont, text, myTextXOfs, myTextYOfs, + width - myTextXOfs * 2, myHeight - myTextYOfs * 2, kTextColor); + + myTipShown = true; + myDialog.setDirtyChain(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToolTip::render() { - if(myWidget != nullptr) - mySurface->render(); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ToolTip::update(int x, int y) -{ - if(myWidget != nullptr && x != myPos.x || y != myPos.y) - release(); - myPos.x = x; - myPos.y = y; + if(myTipShown) + mySurface->render(), cerr << " render tooltip" << endl; } diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx index efcf3bafe..b3bbf547a 100644 --- a/src/gui/ToolTip.hxx +++ b/src/gui/ToolTip.hxx @@ -33,50 +33,59 @@ class FBSurface; class ToolTip { -public: - // Maximum tooltip length - static constexpr uInt32 MAX_LEN = 80; + public: + // Maximum tooltip length + static constexpr uInt32 MAX_LEN = 80; - ToolTip(Dialog& dialog, const GUI::Font& font); - ~ToolTip() = default; + ToolTip(Dialog& dialog, const GUI::Font& font); + ~ToolTip() = default; - /** - Request a tooltip display - */ - void request(const Widget* widget); + /** + Request a tooltip display. + */ + void request(); + /** + Hide a displayed tooltip and reset the timer. + */ + void hide(); - /** - Hide an existing tooltip (if displayed) - */ - void release(); + /** + Hide a displayed tooltip and reset the timer slowly. + This allows faster tip display of the next tip. + */ + void release(); - /** - Update with current mouse position - */ - void update(int x, int y); + /** + Update focussed widget and current mouse position. + */ + void update(const Widget* widget, Common::Point pos); - /* - Render the tooltip - */ - void render(); + /* + Render the tooltip + */ + void render(); -private: - static constexpr uInt32 DELAY_TIME = 45; // display delay - //static constexpr int TEXT_Y_OFS = 2; + private: + void show(); - Dialog& myDialog; - const GUI::Font& myFont; + private: + static constexpr uInt32 DELAY_TIME = 45; // display delay - const Widget* myWidget{nullptr}; - uInt32 myTimer{0}; - Common::Point myPos; - uInt32 myWidth{0}; - uInt32 myHeight{0}; - uInt32 myScale{1}; - uInt32 myTextXOfs{0}; - uInt32 myTextYOfs{0}; - shared_ptr mySurface; + Dialog& myDialog; + const GUI::Font& myFont; + const Widget* myTipWidget{nullptr}; + const Widget* myFocusWidget{nullptr}; + + uInt32 myTimer{0}; + Common::Point myPos; + uInt32 myWidth{0}; + uInt32 myHeight{0}; + uInt32 myTextXOfs{0}; + uInt32 myTextYOfs{0}; + bool myTipShown{false}; + uInt32 myScale{1}; + shared_ptr mySurface; }; #endif diff --git a/src/gui/VideoAudioDialog.cxx b/src/gui/VideoAudioDialog.cxx index d34f0a330..def965aa6 100644 --- a/src/gui/VideoAudioDialog.cxx +++ b/src/gui/VideoAudioDialog.cxx @@ -129,7 +129,7 @@ void VideoAudioDialog::addDisplayTab() // TIA interpolation myTIAInterpolate = new CheckboxWidget(myTab, _font, xpos, ypos + 1, "Interpolation "); - myTIAInterpolate->setToolTip("Blur the emulated display."); + myTIAInterpolate->setToolTip("Blur emulated display."); wid.push_back(myTIAInterpolate); ypos += lineHeight + VGAP * 4; @@ -147,14 +147,14 @@ void VideoAudioDialog::addDisplayTab() // FS stretch myUseStretch = new CheckboxWidget(myTab, _font, xpos + INDENT, ypos + 1, "Stretch"); - myUseStretch->setToolTip("Stretch the emulated display to fill the whole screen."); + myUseStretch->setToolTip("Stretch emulated display to fill whole screen."); wid.push_back(myUseStretch); #ifdef ADAPTABLE_REFRESH_SUPPORT // Adapt refresh rate ypos += lineHeight + VGAP; myRefreshAdapt = new CheckboxWidget(myTab, _font, xpos + INDENT, ypos + 1, "Adapt display refresh rate"); - myRefreshAdapt->setToolTip("Select the optimal display refresh rate for each ROM."); + myRefreshAdapt->setToolTip("Select optimal display refresh rate for each ROM."); wid.push_back(myRefreshAdapt); #else myRefreshAdapt = nullptr; @@ -181,7 +181,7 @@ void VideoAudioDialog::addDisplayTab() "V-Size adjust", lwidth, kVSizeChanged, fontWidth * 7, "%", 0, true); myVSizeAdjust->setMinValue(-5); myVSizeAdjust->setMaxValue(5); myVSizeAdjust->setTickmarkIntervals(2); - myVSizeAdjust->setToolTip("Adapt vertical size to emulated TV display."); + myVSizeAdjust->setToolTip("Adjust vertical size to match emulated TV display."); wid.push_back(myVSizeAdjust); diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index a7fb55561..cfb933965 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -75,8 +75,8 @@ void Widget::tick() { if(isEnabled()) { - if(hasMouseFocus() && hasToolTip()) - dialog().tooltip().request(this); + if(wantsToolTip()) + dialog().tooltip().request(); // Recursively tick widget and all child dialogs and widgets Widget* w = _firstWidget; diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 33c6a3e20..45c676fe2 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -26,6 +26,7 @@ class Dialog; #include #include "bspf.hxx" +#include "Rect.hxx" #include "Event.hxx" #include "EventHandlerConstants.hxx" #include "FrameBufferConstants.hxx" @@ -105,8 +106,8 @@ class Widget : public GuiObject void setShadowColor(ColorId color) { _shadowcolor = color; setDirty(); } void setToolTip(const string& text); - virtual string getToolTip(int x = 0, int y = 0) const { return _toolTipText; } - virtual bool hasToolTip() const { return !_toolTipText.empty(); } + virtual string getToolTip(Common::Point pos) const { return _toolTipText; } + virtual bool changedToolTip(Common::Point oldPos, Common::Point newPos) const { return false; } virtual void loadConfig() { } @@ -120,6 +121,9 @@ class Widget : public GuiObject void releaseFocus() override { assert(_boss); _boss->releaseFocus(); } + virtual bool wantsToolTip() const { return hasMouseFocus() && hasToolTip(); } + virtual bool hasToolTip() const { return !_toolTipText.empty(); } + // By default, delegate unhandled commands to the boss void handleCommand(CommandSender* sender, int cmd, int data, int id) override { assert(_boss); _boss->handleCommand(sender, cmd, data, id); } From 35971d3353a78292a31126d61fb7d587abc83ffe Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 17 Nov 2020 13:06:11 +0100 Subject: [PATCH 053/107] added considering bit order in PF pixel tooltip display removed unused click count from ToggleWidget --- src/debugger/gui/TiaWidget.cxx | 2 +- src/debugger/gui/ToggleBitWidget.cxx | 2 +- src/debugger/gui/TogglePixelWidget.cxx | 5 +++-- src/debugger/gui/TogglePixelWidget.hxx | 4 ++-- src/debugger/gui/ToggleWidget.cxx | 25 ++++++++++++++++--------- src/debugger/gui/ToggleWidget.hxx | 7 ++++--- 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/debugger/gui/TiaWidget.cxx b/src/debugger/gui/TiaWidget.cxx index 5f6ba56ab..20aba0763 100644 --- a/src/debugger/gui/TiaWidget.cxx +++ b/src/debugger/gui/TiaWidget.cxx @@ -563,7 +563,7 @@ TiaWidget::TiaWidget(GuiObject* boss, const GUI::Font& lfont, new StaticTextWidget(boss, lfont, xpos, ypos+2, 2*fontWidth, fontHeight, "PF", TextAlign::Left); xpos += 2*fontWidth + 5; - myPF[0] = new TogglePixelWidget(boss, nfont, xpos, ypos+1, 4, 1); + myPF[0] = new TogglePixelWidget(boss, nfont, xpos, ypos+1, 4, 1, 4); myPF[0]->setTarget(this); myPF[0]->setID(kPF0ID); addFocusWidget(myPF[0]); diff --git a/src/debugger/gui/ToggleBitWidget.cxx b/src/debugger/gui/ToggleBitWidget.cxx index 479f31225..39ca7dcce 100644 --- a/src/debugger/gui/ToggleBitWidget.cxx +++ b/src/debugger/gui/ToggleBitWidget.cxx @@ -26,7 +26,7 @@ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ToggleBitWidget::ToggleBitWidget(GuiObject* boss, const GUI::Font& font, int x, int y, int cols, int rows, int colchars) - : ToggleWidget(boss, font, x, y, cols, rows, 1) + : ToggleWidget(boss, font, x, y, cols, rows) { _rowHeight = font.getLineHeight(); _colWidth = colchars * font.getMaxCharWidth() + 8; diff --git a/src/debugger/gui/TogglePixelWidget.cxx b/src/debugger/gui/TogglePixelWidget.cxx index fdc1609bc..b7dd5dbdf 100644 --- a/src/debugger/gui/TogglePixelWidget.cxx +++ b/src/debugger/gui/TogglePixelWidget.cxx @@ -24,8 +24,9 @@ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TogglePixelWidget::TogglePixelWidget(GuiObject* boss, const GUI::Font& font, - int x, int y, int cols, int rows) - : ToggleWidget(boss, font, x, y, cols, rows, 1) + int x, int y, int cols, int rows, + int shiftBits) + : ToggleWidget(boss, font, x, y, cols, rows, shiftBits) { _rowHeight = _colWidth = font.getLineHeight(); diff --git a/src/debugger/gui/TogglePixelWidget.hxx b/src/debugger/gui/TogglePixelWidget.hxx index 6177cac45..c748210f2 100644 --- a/src/debugger/gui/TogglePixelWidget.hxx +++ b/src/debugger/gui/TogglePixelWidget.hxx @@ -25,7 +25,8 @@ class TogglePixelWidget : public ToggleWidget { public: TogglePixelWidget(GuiObject* boss, const GUI::Font& font, - int x, int y, int cols, int rows); + int x, int y, int cols = 1, int rows = 1, + int shiftBits = 0); ~TogglePixelWidget() override = default; void setColor(ColorId color) { _pixelColor = color; } @@ -42,7 +43,6 @@ class TogglePixelWidget : public ToggleWidget private: ColorId _pixelColor{kNone}, _backgroundColor{kDlgColor}; - bool _swapBits{false}; bool _crossBits{false}; private: diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index 3d2fca262..60f4ee154 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -23,8 +23,7 @@ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ToggleWidget::ToggleWidget(GuiObject* boss, const GUI::Font& font, - int x, int y, int cols, int rows, - int clicksToChange) + int x, int y, int cols, int rows, int shiftBits) : Widget(boss, font, x, y, 16, 16), CommandSender(boss), _rows(rows), @@ -34,7 +33,7 @@ ToggleWidget::ToggleWidget(GuiObject* boss, const GUI::Font& font, _rowHeight(0), _colWidth(0), _selectedItem(0), - _clicksToChange(clicksToChange), + _shiftBits(shiftBits), _editable(true) { _flags = Widget::FLAG_ENABLED | Widget::FLAG_CLEARBG | Widget::FLAG_RETAIN_FOCUS | @@ -70,7 +69,7 @@ void ToggleWidget::handleMouseUp(int x, int y, MouseButton b, int clickCount) // If this was a double click and the mouse is still over the selected item, // send the double click command - if (clickCount == _clicksToChange && (_selectedItem == findItem(x, y))) + if (clickCount == 1 && (_selectedItem == findItem(x, y))) { _stateList[_selectedItem] = !_stateList[_selectedItem]; _changedList[_selectedItem] = !_changedList[_selectedItem]; @@ -225,11 +224,19 @@ string ToggleWidget::getToolTip(Common::Point pos) const const int idx = getToolTipIndex(pos); Int32 val = 0; - for(int col = 0; col < _cols; ++col) - { - val <<= 1; - val += _stateList[idx + col]; - } + if(_swapBits) + for(int col = _cols - 1; col >= 0; --col) + { + val <<= 1; + val += _stateList[idx + col]; + } + else + for(int col = 0; col < _cols; ++col) + { + val <<= 1; + val += _stateList[idx + col]; + } + val <<= _shiftBits; const string hex = Common::Base::toString(val, Common::Base::Fmt::_16); const string dec = Common::Base::toString(val, Common::Base::Fmt::_10); diff --git a/src/debugger/gui/ToggleWidget.hxx b/src/debugger/gui/ToggleWidget.hxx index 060a52e13..5f4ec13a7 100644 --- a/src/debugger/gui/ToggleWidget.hxx +++ b/src/debugger/gui/ToggleWidget.hxx @@ -33,8 +33,8 @@ class ToggleWidget : public Widget, public CommandSender public: ToggleWidget(GuiObject* boss, const GUI::Font& font, - int x, int y, int cols, int rows, - int clicksToChange = 2); + int x, int y, int cols = 1, int rows = 1, + int shiftBits = 0); ~ToggleWidget() override = default; const BoolArray& getState() { return _stateList; } @@ -60,8 +60,9 @@ class ToggleWidget : public Widget, public CommandSender int _rowHeight; // explicitly set in child classes int _colWidth; // explicitly set in child classes int _selectedItem; - int _clicksToChange; // number of clicks to register a change bool _editable; + bool _swapBits{false}; + int _shiftBits{0}; // shift bits for tooltip display BoolArray _stateList; BoolArray _changedList; From 92b77f32c474700e1583816aa4543ea2f5ab17ea Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 17 Nov 2020 13:36:12 +0100 Subject: [PATCH 054/107] enhanced GPRx bits display in debugger, now considers reflection --- src/debugger/gui/TiaWidget.cxx | 12 ++++++++---- src/debugger/gui/TogglePixelWidget.hxx | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/debugger/gui/TiaWidget.cxx b/src/debugger/gui/TiaWidget.cxx index 20aba0763..c330bf386 100644 --- a/src/debugger/gui/TiaWidget.cxx +++ b/src/debugger/gui/TiaWidget.cxx @@ -919,10 +919,14 @@ void TiaWidget::handleCommand(CommandSender* sender, int cmd, int data, int id) case kRefP0ID: tia.refP0(myRefP0->getState() ? 1 : 0); + myGRP0->setIntState(myGRP0->getIntState(), !myRefP0->getState()); + myGRP0Old->setIntState(myGRP0Old->getIntState(), !myRefP0->getState()); break; case kRefP1ID: tia.refP1(myRefP1->getState() ? 1 : 0); + myGRP1->setIntState(myGRP1->getIntState(), !myRefP1->getState()); + myGRP1Old->setIntState(myGRP1Old->getIntState(), !myRefP1->getState()); break; case kDelP0ID: @@ -1043,8 +1047,8 @@ void TiaWidget::loadConfig() myGRP0Old->setColor(kBGColorLo); myGRP0Old->setCrossed(true); } - myGRP0->setIntState(state.gr[TiaState::P0], false); - myGRP0Old->setIntState(state.gr[TiaState::P0+2], false); + myGRP0->setIntState(state.gr[TiaState::P0], state.ref[TiaState::P0]); + myGRP0Old->setIntState(state.gr[TiaState::P0+2], state.ref[TiaState::P0]); // posP0 myPosP0->setList(0, state.pos[TiaState::P0], @@ -1079,8 +1083,8 @@ void TiaWidget::loadConfig() myGRP1Old->setColor(kBGColorLo); myGRP1Old->setCrossed(true); } - myGRP1->setIntState(state.gr[TiaState::P1], false); - myGRP1Old->setIntState(state.gr[TiaState::P1+2], false); + myGRP1->setIntState(state.gr[TiaState::P1], state.ref[TiaState::P1]); + myGRP1Old->setIntState(state.gr[TiaState::P1+2], state.ref[TiaState::P1]); // posP1 myPosP1->setList(0, state.pos[TiaState::P1], diff --git a/src/debugger/gui/TogglePixelWidget.hxx b/src/debugger/gui/TogglePixelWidget.hxx index c748210f2..fe63cd964 100644 --- a/src/debugger/gui/TogglePixelWidget.hxx +++ b/src/debugger/gui/TogglePixelWidget.hxx @@ -36,7 +36,7 @@ class TogglePixelWidget : public ToggleWidget void setState(const BoolArray& state); - void setIntState(int value, bool swap); + void setIntState(int value, bool swap = false); int getIntState(); void setCrossed(bool enable) { _crossBits = enable; } From 9bb6959dd857fc2025a97b730c0e019e7a7621af Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 17 Nov 2020 18:10:54 +0100 Subject: [PATCH 055/107] aligned tooltip font to dialog font improved debugger tooltip display added tooltips for RomListWidget bytes --- src/debugger/gui/DataGridWidget.cxx | 11 ++--- src/debugger/gui/DebuggerDialog.cxx | 2 + src/debugger/gui/RomListWidget.cxx | 63 +++++++++++++++++++++++++++++ src/debugger/gui/RomListWidget.hxx | 7 ++++ src/debugger/gui/ToggleWidget.cxx | 12 +++--- src/gui/LauncherDialog.cxx | 6 ++- src/gui/ToolTip.cxx | 34 +++++++++++----- src/gui/ToolTip.hxx | 4 +- 8 files changed, 116 insertions(+), 23 deletions(-) diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index d11375d29..9e75a955f 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -583,13 +583,14 @@ int DataGridWidget::getToolTipIndex(Common::Point pos) const string DataGridWidget::getToolTip(Common::Point pos) const { const Int32 val = _valueList[getToolTipIndex(pos)]; - const string hex = Common::Base::toString(val, Common::Base::Fmt::_16); - const string dec = Common::Base::toString(val, Common::Base::Fmt::_10); - const string bin = Common::Base::toString(val, Common::Base::Fmt::_2); ostringstream buf; - // TODO: time leading spaces and zeroes - buf << "$" << hex << " = #" << dec << " = %" << bin; + buf << _toolTipText + << "$" << Common::Base::toString(val, Common::Base::Fmt::_16) + << " = #" << val; + if(val < 0x100) + buf << " = %" << Common::Base::toString(val, Common::Base::Fmt::_2); + return buf.str(); } diff --git a/src/debugger/gui/DebuggerDialog.cxx b/src/debugger/gui/DebuggerDialog.cxx index b8c0662ee..07e530ac6 100644 --- a/src/debugger/gui/DebuggerDialog.cxx +++ b/src/debugger/gui/DebuggerDialog.cxx @@ -18,6 +18,7 @@ #include "Cart.hxx" #include "Widget.hxx" #include "Dialog.hxx" +#include "ToolTip.hxx" #include "Settings.hxx" #include "StellaKeys.hxx" #include "EventHandler.hxx" @@ -406,6 +407,7 @@ void DebuggerDialog::createFont() break; } } + tooltip().setFont(*myNFont); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 58d392ee9..e595fc6aa 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -442,6 +442,69 @@ void RomListWidget::lostFocusWidget() abortEditMode(); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Common::Point RomListWidget::getToolTipIndex(Common::Point pos) const +{ + const Common::Rect& r = getEditRect(); + const int col = (pos.x - r.x() - getAbsX()) / _font.getMaxCharWidth(); + const int row = (pos.y - getAbsY()) / _lineHeight; + + if(col < 0) + return Common::Point(-1, -1); + else + return Common::Point(col, row + _currentPos); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string RomListWidget::getToolTip(Common::Point pos) const +{ + const Common::Point idx = getToolTipIndex(pos); + + if(idx.y == -1) + return EmptyString; + + const string bytes = myDisasm->list[idx.y].bytes; + + if(bytes.length() < idx.x + 1) + return EmptyString; + + Int32 val; + if(bytes.length() == 8 && bytes[2] != ' ') + { + // Binary value + val = stol(bytes, 0, 2); + } + else + { + // hex 1..3 values + if(idx.x % 3 == 2) + // Skip gaps between hex values + return EmptyString; + + // Get one hex byte + const string valStr = bytes.substr((idx.x / 3) * 3, 2); + + val = stol(valStr, 0, 16); + + } + ostringstream buf; + + buf << _toolTipText + << "$" << Common::Base::toString(val, Common::Base::Fmt::_16) + << " = #" << val; + if(val < 0x100) + buf << " = %" << Common::Base::toString(val, Common::Base::Fmt::_2); + + return buf.str(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool RomListWidget::changedToolTip(Common::Point oldPos, Common::Point newPos) const +{ + return getToolTipIndex(oldPos) != getToolTipIndex(newPos); +} + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void RomListWidget::drawWidget(bool hilite) { diff --git a/src/debugger/gui/RomListWidget.hxx b/src/debugger/gui/RomListWidget.hxx index 37473d12f..3f26d217f 100644 --- a/src/debugger/gui/RomListWidget.hxx +++ b/src/debugger/gui/RomListWidget.hxx @@ -56,6 +56,9 @@ class RomListWidget : public EditableWidget void setSelected(int item); void setHighlighted(int item); + string getToolTip(Common::Point pos) const override; + bool changedToolTip(Common::Point oldPos, Common::Point newPos) const override; + protected: void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; @@ -77,11 +80,15 @@ class RomListWidget : public EditableWidget void endEditMode() override; void abortEditMode() override; void lostFocusWidget() override; + + bool hasToolTip() const override { return true; } + void scrollToSelected() { scrollToCurrent(_selectedItem); } void scrollToHighlighted() { scrollToCurrent(_highlightedItem); } private: void scrollToCurrent(int item); + Common::Point getToolTipIndex(Common::Point pos) const; private: unique_ptr myMenu; diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index 60f4ee154..e20544783 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -223,6 +223,7 @@ string ToggleWidget::getToolTip(Common::Point pos) const { const int idx = getToolTipIndex(pos); Int32 val = 0; + ostringstream buf; if(_swapBits) for(int col = _cols - 1; col >= 0; --col) @@ -238,13 +239,12 @@ string ToggleWidget::getToolTip(Common::Point pos) const } val <<= _shiftBits; - const string hex = Common::Base::toString(val, Common::Base::Fmt::_16); - const string dec = Common::Base::toString(val, Common::Base::Fmt::_10); - const string bin = Common::Base::toString(val, Common::Base::Fmt::_2); - ostringstream buf; + buf << _toolTipText + << "$" << Common::Base::toString(val, Common::Base::Fmt::_16) + << " = #" << val; + if(val < 0x100) + buf << " = %" << Common::Base::toString(val, Common::Base::Fmt::_2); - // TODO: time leading spaces and zeroes - buf << "$" << hex << " = #" << dec << " = %" << bin; return buf.str(); } diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 9c140e15c..5092299ec 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -30,6 +30,7 @@ #include "StellaSettingsDialog.hxx" #include "WhatsNewDialog.hxx" #include "MessageBox.hxx" +#include "ToolTip.hxx" #include "OSystem.hxx" #include "FrameBuffer.hxx" #include "FBSurface.hxx" @@ -56,8 +57,7 @@ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, int x, int y, int w, int h) - : Dialog(osystem, parent, osystem.frameBuffer().launcherFont(), "", - x, y, w, h) + : Dialog(osystem, parent, x, y, w, h) { myUseMinimalUI = instance().settings().getBool("minimal_ui"); @@ -80,6 +80,8 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, const string& lblAllFiles = "Show all files"; const string& lblFound = "XXXX items found"; + tooltip().setFont(font); + lwidth = font.getStringWidth(lblRom); lwidth2 = font.getStringWidth(lblAllFiles) + CheckboxWidget::boxSize(font); int lwidth3 = font.getStringWidth(lblFilter); diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 36c8b1b20..85f5bedf4 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -32,16 +32,24 @@ // + disable when in edit mode // - option to disable tips // - multi line tips -// - nicer formating of DataDridWidget tip -// - allow reversing ToogleWidget (TooglePixelWidget) -// - shift checkbox bits +// + nicer formating of DataDridWidget tip +// + allow reversing ToogleWidget (TooglePixelWidget) +// + shift checkbox bits // - RomListWidget (hex codes, maybe disassembly operands) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ToolTip::ToolTip(Dialog& dialog, const GUI::Font& font) - : myDialog(dialog), - myFont(font) + : myDialog(dialog) { + myScale = myDialog.instance().frameBuffer().hidpiScaleFactor(); + + setFont(font); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ToolTip::setFont(const GUI::Font& font) +{ + myFont = &font; const int fontWidth = font.getMaxCharWidth(), fontHeight = font.getFontHeight(); @@ -51,7 +59,6 @@ ToolTip::ToolTip(Dialog& dialog, const GUI::Font& font) myHeight = fontHeight + myTextYOfs * 2; mySurface = myDialog.instance().frameBuffer().allocateSurface(myWidth, myHeight); - myScale = myDialog.instance().frameBuffer().hidpiScaleFactor(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -86,7 +93,7 @@ void ToolTip::release() { if(myTipShown) { - myTimer = DELAY_TIME; + myTimer = DELAY_TIME - 1; myTipShown = false; myDialog.setDirtyChain(); @@ -118,7 +125,16 @@ void ToolTip::show() const uInt32 V_GAP = 1; const uInt32 H_CURSOR = 18; string text = myTipWidget->getToolTip(myPos); - uInt32 width = std::min(myWidth, myFont.getStringWidth(text) + myTextXOfs * 2); + + myTipShown = true; + if(text.empty()) + { + release(); + return; + } + + uInt32 width = std::min(myWidth, myFont->getStringWidth(text) + myTextXOfs * 2); + //uInt32 height = std::min(myHeight, font.getFontHeight() + myTextYOfs * 2); // Note: The rects include HiDPI scaling const Common::Rect imageRect = myDialog.instance().frameBuffer().imageRect(); const Common::Rect dialogRect = myDialog.surface().dstRect(); @@ -142,7 +158,7 @@ void ToolTip::show() mySurface->frameRect(0, 0, width, myHeight, kColor); mySurface->fillRect(1, 1, width - 2, myHeight - 2, kWidColor); - mySurface->drawString(myFont, text, myTextXOfs, myTextYOfs, + mySurface->drawString(*myFont, text, myTextXOfs, myTextYOfs, width - myTextXOfs * 2, myHeight - myTextYOfs * 2, kTextColor); myTipShown = true; diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx index b3bbf547a..879200c06 100644 --- a/src/gui/ToolTip.hxx +++ b/src/gui/ToolTip.hxx @@ -40,6 +40,8 @@ class ToolTip ToolTip(Dialog& dialog, const GUI::Font& font); ~ToolTip() = default; + void setFont(const GUI::Font& font); + /** Request a tooltip display. */ @@ -73,7 +75,7 @@ class ToolTip static constexpr uInt32 DELAY_TIME = 45; // display delay Dialog& myDialog; - const GUI::Font& myFont; + const GUI::Font* myFont{nullptr}; const Widget* myTipWidget{nullptr}; const Widget* myFocusWidget{nullptr}; From 94ed04469761e2b7b35eeb1611b5eb36dcd2e12b Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 17 Nov 2020 19:41:23 +0100 Subject: [PATCH 056/107] added tooltips to TiaInfoWidget and CpuWidget --- src/debugger/gui/CpuWidget.cxx | 6 +++--- src/debugger/gui/RomListWidget.cxx | 2 +- src/debugger/gui/TiaInfoWidget.cxx | 11 +++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/debugger/gui/CpuWidget.cxx b/src/debugger/gui/CpuWidget.cxx index ea6fbb96f..a8719f448 100644 --- a/src/debugger/gui/CpuWidget.cxx +++ b/src/debugger/gui/CpuWidget.cxx @@ -85,18 +85,19 @@ CpuWidget::CpuWidget(GuiObject* boss, const GUI::Font& lfont, const GUI::Font& n _w = lwidth + myPCGrid->getWidth() + myPCLabel->getWidth() + 20; // Create labels showing the source of data for SP/A/X/Y registers + const std::array labels = { "SP", "A", "X", "Y" }; xpos += myCpuGridBinValue->getWidth() + 20; int src_y = ypos, src_w = (max_w - xpos + x) - 10; for(int i = 0; i < 4; ++i) { myCpuDataSrc[i] = new EditTextWidget(boss, nfont, xpos, src_y, src_w, fontHeight + 1); + myCpuDataSrc[i]->setToolTip("Source label of last load into " + labels[i] + "."); myCpuDataSrc[i]->setEditable(false, true); src_y += fontHeight + 2; } // Add labels for other CPU registers xpos = x; - const std::array labels = { "SP ", "A ", "X ", "Y " }; for(int row = 0; row < 4; ++row) { new StaticTextWidget(boss, lfont, xpos, ypos + row*lineHeight + 2, @@ -139,10 +140,9 @@ CpuWidget::CpuWidget(GuiObject* boss, const GUI::Font& lfont, const GUI::Font& n xpos = myCpuDataSrc[0]->getLeft(); new StaticTextWidget(boss, lfont, xpos - fontWidth * 4.5, ypos + 2, "Dest"); myCpuDataDest = new EditTextWidget(boss, nfont, xpos, ypos, src_w, fontHeight + 1); + myCpuDataDest->setToolTip("Destination label of last store."); myCpuDataDest->setEditable(false, true); - - _h = ypos + myPSRegister->getHeight() - y; } diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index e595fc6aa..72f3a0b03 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -476,7 +476,7 @@ string RomListWidget::getToolTip(Common::Point pos) const } else { - // hex 1..3 values + // 1..3 hex values if(idx.x % 3 == 2) // Skip gaps between hex values return EmptyString; diff --git a/src/debugger/gui/TiaInfoWidget.cxx b/src/debugger/gui/TiaInfoWidget.cxx index a02b2365d..2690f8b4f 100644 --- a/src/debugger/gui/TiaInfoWidget.cxx +++ b/src/debugger/gui/TiaInfoWidget.cxx @@ -60,30 +60,35 @@ TiaInfoWidget::TiaInfoWidget(GuiObject* boss, const GUI::Font& lfont, xpos = x; new StaticTextWidget(boss, lfont, xpos, ypos + 1, longstr ? "Frame Cycls" : "F. Cycls"); myFrameCycles = new EditTextWidget(boss, nfont, xpos + lwidth, ypos - 1, fwidth, lineHeight); + myFrameCycles->setToolTip("CPU cycles executed this frame."); myFrameCycles->setEditable(false, true); // Left: WSync Cycles ypos += lineHeight + VGAP; new StaticTextWidget(boss, lfont, xpos, ypos + 1, longstr ? "WSync Cycls" : "WSync C."); myWSyncCylces = new EditTextWidget(boss, nfont, xpos + lwidth, ypos - 1, fwidth, lineHeight); + myWSyncCylces->setToolTip("CPU cycles used for WSYNC this frame."); myWSyncCylces->setEditable(false, true); // Left: Timer Cycles ypos += lineHeight + VGAP; new StaticTextWidget(boss, lfont, xpos, ypos + 1, longstr ? "Timer Cycls" : "Timer C."); myTimerCylces = new EditTextWidget(boss, nfont, xpos + lwidth, ypos - 1, fwidth, lineHeight); + myTimerCylces->setToolTip("CPU cycles roughly used for INTIM reads this frame."); myTimerCylces->setEditable(false, true); // Left: Total Cycles ypos += lineHeight + VGAP; new StaticTextWidget(boss, lfont, xpos, ypos + 1, "Total"); myTotalCycles = new EditTextWidget(boss, nfont, xpos + lwidth8, ypos - 1, twidth, lineHeight); + myTotalCycles->setToolTip("Total CPU cycles executed for this session (E notation)."); myTotalCycles->setEditable(false, true); // Left: Delta Cycles ypos += lineHeight + VGAP; new StaticTextWidget(boss, lfont, xpos, ypos + 1, "Delta"); myDeltaCycles = new EditTextWidget(boss, nfont, xpos + lwidth8, ypos - 1, twidth, lineHeight); + myDeltaCycles->setToolTip("CPU cycles executed since last debug break."); myDeltaCycles->setEditable(false, true); // Right column @@ -93,6 +98,7 @@ TiaInfoWidget::TiaInfoWidget(GuiObject* boss, const GUI::Font& lfont, // Right: Frame Count new StaticTextWidget(boss, lfont, xpos, ypos + 1, longstr ? "Frame Cnt." : "Frame"); myFrameCount = new EditTextWidget(boss, nfont, xpos + lwidthR, ypos - 1, fwidth, lineHeight); + myFrameCount->setToolTip("Total number of frames executed this session."); myFrameCount->setEditable(false, true); lwidth = lfont.getStringWidth(longstr ? "Color Clock " : "Pixel Pos ") + LGAP; @@ -102,28 +108,33 @@ TiaInfoWidget::TiaInfoWidget(GuiObject* boss, const GUI::Font& lfont, ypos += lineHeight + VGAP; new StaticTextWidget(boss, lfont, xpos, ypos + 1, longstr ? "Scanline" : "Scn Ln"); myScanlineCountLast = new EditTextWidget(boss, nfont, xpos + lwidth, ypos - 1, fwidth, lineHeight); + myScanlineCountLast->setToolTip("Number of scanlines of last frame."); myScanlineCountLast->setEditable(false, true); myScanlineCount = new EditTextWidget(boss, nfont, xpos + lwidth - myScanlineCountLast->getWidth() - 2, ypos - 1, fwidth, lineHeight); + myScanlineCount->setToolTip("Current scanline of this frame."); myScanlineCount->setEditable(false, true); // Right: Scan Cycle ypos += lineHeight + VGAP; new StaticTextWidget(boss, lfont, xpos, ypos + 1, longstr ? "Scan Cycle" : "Scn Cycle"); myScanlineCycles = new EditTextWidget(boss, nfont, xpos + lwidth, ypos - 1, fwidth, lineHeight); + myScanlineCycles->setToolTip("CPU cycles in current scanline."); myScanlineCycles->setEditable(false, true); // Right: Pixel Pos ypos += lineHeight + VGAP; new StaticTextWidget(boss, lfont, xpos, ypos + 1, "Pixel Pos"); myPixelPosition = new EditTextWidget(boss, nfont, xpos + lwidth, ypos - 1, fwidth, lineHeight); + myPixelPosition->setToolTip("Pixel position in current scanline."); myPixelPosition->setEditable(false, true); // Right: Color Clock ypos += lineHeight + VGAP; new StaticTextWidget(boss, lfont, xpos, ypos + 1, longstr ? "Color Clock" : "Color Clk"); myColorClocks = new EditTextWidget(boss, nfont, xpos + lwidth, ypos - 1, fwidth, lineHeight); + myColorClocks->setToolTip("Color clocks in current scanline."); myColorClocks->setEditable(false, true); // Calculate actual dimensions From a660861008adf479525c7283ab3fb86c6af7251f Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Tue, 17 Nov 2020 18:37:10 -0330 Subject: [PATCH 057/107] Fixed minor clang warnings, and implemented clang-tidy suggestions. --- src/debugger/gui/RomListWidget.cxx | 6 +++--- src/debugger/gui/ToggleWidget.cxx | 8 +------- src/debugger/gui/ToggleWidget.hxx | 16 ++++++++-------- src/gui/ToolTip.cxx | 7 +++++-- src/gui/ToolTip.hxx | 4 ++-- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 72f3a0b03..504f7443d 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -465,14 +465,14 @@ string RomListWidget::getToolTip(Common::Point pos) const const string bytes = myDisasm->list[idx.y].bytes; - if(bytes.length() < idx.x + 1) + if(bytes.length() < size_t(idx.x + 1)) return EmptyString; Int32 val; if(bytes.length() == 8 && bytes[2] != ' ') { // Binary value - val = stol(bytes, 0, 2); + val = static_cast(stol(bytes, 0, 2)); } else { @@ -484,7 +484,7 @@ string RomListWidget::getToolTip(Common::Point pos) const // Get one hex byte const string valStr = bytes.substr((idx.x / 3) * 3, 2); - val = stol(valStr, 0, 16); + val = static_cast(stol(valStr, 0, 16)); } ostringstream buf; diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index e20544783..2a4187c6d 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -28,13 +28,7 @@ ToggleWidget::ToggleWidget(GuiObject* boss, const GUI::Font& font, CommandSender(boss), _rows(rows), _cols(cols), - _currentRow(0), - _currentCol(0), - _rowHeight(0), - _colWidth(0), - _selectedItem(0), - _shiftBits(shiftBits), - _editable(true) + _shiftBits(shiftBits) { _flags = Widget::FLAG_ENABLED | Widget::FLAG_CLEARBG | Widget::FLAG_RETAIN_FOCUS | Widget::FLAG_WANTS_RAWDATA; diff --git a/src/debugger/gui/ToggleWidget.hxx b/src/debugger/gui/ToggleWidget.hxx index 5f4ec13a7..3f74237b0 100644 --- a/src/debugger/gui/ToggleWidget.hxx +++ b/src/debugger/gui/ToggleWidget.hxx @@ -53,14 +53,14 @@ class ToggleWidget : public Widget, public CommandSender bool hasToolTip() const override { return true; } protected: - int _rows; - int _cols; - int _currentRow; - int _currentCol; - int _rowHeight; // explicitly set in child classes - int _colWidth; // explicitly set in child classes - int _selectedItem; - bool _editable; + int _rows{0}; + int _cols{0}; + int _currentRow{0}; + int _currentCol{0}; + int _rowHeight{0}; // explicitly set in child classes + int _colWidth{0}; // explicitly set in child classes + int _selectedItem{0}; + bool _editable{true}; bool _swapBits{false}; int _shiftBits{0}; // shift bits for tooltip display diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 85f5bedf4..77b5d5e20 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -62,7 +62,7 @@ void ToolTip::setFont(const GUI::Font& font) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ToolTip::update(const Widget* widget, Common::Point pos) +void ToolTip::update(const Widget* widget, const Common::Point& pos) { if(myTipWidget != widget) { @@ -70,7 +70,10 @@ void ToolTip::update(const Widget* widget, Common::Point pos) release(); } if(myTipShown && myTipWidget->changedToolTip(myPos, pos)) - myPos = pos, show(); + { + myPos = pos; + show(); + } else myPos = pos; } diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx index 879200c06..17ff2e7ec 100644 --- a/src/gui/ToolTip.hxx +++ b/src/gui/ToolTip.hxx @@ -59,9 +59,9 @@ class ToolTip void release(); /** - Update focussed widget and current mouse position. + Update focused widget and current mouse position. */ - void update(const Widget* widget, Common::Point pos); + void update(const Widget* widget, const Common::Point& pos); /* Render the tooltip From d7d813b901e4ecb22dcdca291a62921f0d840d48 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Tue, 17 Nov 2020 18:54:35 -0330 Subject: [PATCH 058/107] Fixed another minor clang warning. --- src/debugger/gui/RomListWidget.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 504f7443d..0d0d6c3fa 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -472,7 +472,7 @@ string RomListWidget::getToolTip(Common::Point pos) const if(bytes.length() == 8 && bytes[2] != ' ') { // Binary value - val = static_cast(stol(bytes, 0, 2)); + val = static_cast(stol(bytes, nullptr, 2)); } else { @@ -484,7 +484,7 @@ string RomListWidget::getToolTip(Common::Point pos) const // Get one hex byte const string valStr = bytes.substr((idx.x / 3) * 3, 2); - val = static_cast(stol(valStr, 0, 16)); + val = static_cast(stol(valStr, nullptr, 16)); } ostringstream buf; From 9aaca0bd4e01c17f4aba012d3120d9d0799cf719 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 18 Nov 2020 17:48:19 +0100 Subject: [PATCH 059/107] improved tooltip handling (better delays, rerender instead of redraw) added tooltip to StringListWidget for shortened texts (e.g. ROM names in launcher) added code for StaticTextWidget tooltip (without setting widget dirty) --- src/debugger/gui/RomListWidget.cxx | 2 +- src/gui/Dialog.cxx | 4 +- src/gui/EditableWidget.cxx | 8 +-- src/gui/EditableWidget.hxx | 1 + src/gui/GuiObject.hxx | 8 +-- src/gui/StringListWidget.cxx | 30 +++++++++ src/gui/StringListWidget.hxx | 9 +++ src/gui/ToolTip.cxx | 104 ++++++++++++++--------------- src/gui/ToolTip.hxx | 12 ++-- src/gui/Widget.cxx | 21 +++++- src/gui/Widget.hxx | 4 +- 11 files changed, 130 insertions(+), 73 deletions(-) diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 0d0d6c3fa..861a4112d 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -40,6 +40,7 @@ RomListWidget::RomListWidget(GuiObject* boss, const GUI::Font& lfont, _textcolorhi = kTextColor; _editMode = false; + _dyCaret = 1; _cols = w / _fontWidth; _rows = h / _lineHeight; @@ -485,7 +486,6 @@ string RomListWidget::getToolTip(Common::Point pos) const const string valStr = bytes.substr((idx.x / 3) * 3, 2); val = static_cast(stol(valStr, nullptr, 16)); - } ostringstream buf; diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index a33b4ffa1..1b58d7729 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -432,9 +432,9 @@ void Dialog::drawDialog() FBSurface& s = surface(); - cerr << endl << "d"; if(isDirty()) { + cerr << endl << "d"; //cerr << "*** draw dialog " << typeid(*this).name() << " ***" << endl; if(clearsBackground()) @@ -464,6 +464,8 @@ void Dialog::drawDialog() clearDirty(); } + else + cerr << endl; // Draw all children drawChain(); diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index 39a4b519f..8ff411f83 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -107,7 +107,7 @@ void EditableWidget::receivedFocusWidget() { _caretTimer = 0; _caretEnabled = true; - dialog().tooltip().release(); + dialog().tooltip().hide(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -379,7 +379,7 @@ void EditableWidget::drawCaretSelection() x += _x; y += _y; - s.fillRect(x - 1, y + 1, w + 1, h - 3, kTextColorHi); + s.fillRect(x - 1, y + 1 + _dyCaret, w + 1, h - 3, kTextColorHi); s.drawString(_font, text, x, y + 1, w, h, kTextColorInv, TextAlign::Left, 0, false); } @@ -397,8 +397,8 @@ void EditableWidget::drawCaretSelection() x += _x; y += _y; - s.vLine(x, y + 1, y + editRect.h() - 3, color); - s.vLine(x - 1, y + 1, y + editRect.h() - 3, color); + s.vLine(x, y + 1 + _dyCaret, y + editRect.h() - 3, color); + s.vLine(x - 1, y + 1 + _dyCaret, y + editRect.h() - 3, color); clearDirty(); } } diff --git a/src/gui/EditableWidget.hxx b/src/gui/EditableWidget.hxx index 6e55cf541..497c2a4e8 100644 --- a/src/gui/EditableWidget.hxx +++ b/src/gui/EditableWidget.hxx @@ -125,6 +125,7 @@ class EditableWidget : public Widget, public CommandSender protected: int _editScrollOffset{0}; bool _editMode{true}; + int _dyCaret{0}; private: TextFilter _filter; diff --git a/src/gui/GuiObject.hxx b/src/gui/GuiObject.hxx index 490df4344..85424a670 100644 --- a/src/gui/GuiObject.hxx +++ b/src/gui/GuiObject.hxx @@ -106,20 +106,20 @@ class GuiObject : public CommandReceiver virtual void tick() = 0; - void setFlags(uInt32 flags) + void setFlags(uInt32 flags, bool updateDirty = true) { uInt32 oldFlags = _flags; _flags |= flags; - if(oldFlags != _flags) + if(updateDirty && oldFlags != _flags) setDirty(); } - void clearFlags(uInt32 flags) + void clearFlags(uInt32 flags, bool updateDirty = true) { uInt32 oldFlags = _flags; _flags &= ~flags; - if(oldFlags != _flags) + if(updateDirty && oldFlags != _flags) setDirty(); } uInt32 getFlags() const { return _flags; } diff --git a/src/gui/StringListWidget.cxx b/src/gui/StringListWidget.cxx index ad5f1e7ec..74cf23e40 100644 --- a/src/gui/StringListWidget.cxx +++ b/src/gui/StringListWidget.cxx @@ -50,6 +50,36 @@ void StringListWidget::setList(const StringList& list) ListWidget::recalc(); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +int StringListWidget::getToolTipIndex(Common::Point pos) const +{ + return (pos.y - getAbsY()) / _lineHeight + _currentPos; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string StringListWidget::getToolTip(Common::Point pos) const +{ + Common::Rect rect = getEditRect(); + const string value = _list[getToolTipIndex(pos)]; + + if(uInt32(_font.getStringWidth(value)) > rect.w()) + return _toolTipText + value; + else + return _toolTipText; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool StringListWidget::changedToolTip(Common::Point oldPos, Common::Point newPos) const +{ + bool ch = getToolTipIndex(oldPos) != getToolTipIndex(newPos) + && getToolTip(oldPos) != getToolTip(newPos); + + if(ch) + cerr << "changed" << endl; + + return ch; +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StringListWidget::drawWidget(bool hilite) { diff --git a/src/gui/StringListWidget.hxx b/src/gui/StringListWidget.hxx index ed5874b91..fb96a9e22 100644 --- a/src/gui/StringListWidget.hxx +++ b/src/gui/StringListWidget.hxx @@ -32,10 +32,16 @@ class StringListWidget : public ListWidget void setList(const StringList& list); bool wantsFocus() const override { return true; } + string getToolTip(Common::Point pos) const override; + bool changedToolTip(Common::Point oldPos, Common::Point newPos) const override; + protected: // display depends on _hasFocus so we have to redraw when focus changes void receivedFocusWidget() override { setDirty(); } void lostFocusWidget() override { setDirty(); } + + bool hasToolTip() const override { return true; } + void drawWidget(bool hilite) override; Common::Rect getEditRect() const override; @@ -43,6 +49,9 @@ class StringListWidget : public ListWidget bool _hilite{false}; int _textOfs{0}; + private: + int getToolTipIndex(Common::Point pos) const; + private: // Following constructors and assignment operators not supported StringListWidget() = delete; diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 77b5d5e20..5d37478db 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -17,6 +17,7 @@ #include "OSystem.hxx" #include "Dialog.hxx" +#include "DialogContainer.hxx" #include "Font.hxx" #include "FrameBuffer.hxx" #include "FBSurface.hxx" @@ -25,17 +26,8 @@ #include "ToolTip.hxx" // TODOs: -// + keep enabled when mouse moves over same widget -// + static position and text for normal widgets -// + moving position and text over e.g. DataGridWidget -// + reenable tip faster -// + disable when in edit mode // - option to disable tips // - multi line tips -// + nicer formating of DataDridWidget tip -// + allow reversing ToogleWidget (TooglePixelWidget) -// + shift checkbox bits -// - RomListWidget (hex codes, maybe disassembly operands) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ToolTip::ToolTip(Dialog& dialog, const GUI::Font& font) @@ -61,21 +53,48 @@ void ToolTip::setFont(const GUI::Font& font) mySurface = myDialog.instance().frameBuffer().allocateSurface(myWidth, myHeight); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ToolTip::request() +{ + // Called each frame when a tooltip is wanted + if(myFocusWidget && myTimer < DELAY_TIME * RELEASE_SPEED) + { + const string tip = myFocusWidget->getToolTip(myMousePos); + + if(!tip.empty()) + { + myTipWidget = myFocusWidget; + myTimer += RELEASE_SPEED; + if(myTimer >= DELAY_TIME * RELEASE_SPEED) + show(tip); + } + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToolTip::update(const Widget* widget, const Common::Point& pos) { + // Called each mouse move + myMousePos = pos; + myFocusWidget = widget; + if(myTipWidget != widget) - { - myFocusWidget = widget; - release(); - } - if(myTipShown && myTipWidget->changedToolTip(myPos, pos)) - { - myPos = pos; - show(); - } + release(false); + + if(!myTipShown) + release(true); else - myPos = pos; + { + if(myTipWidget->changedToolTip(myTipPos, myMousePos)) + { + const string tip = myTipWidget->getToolTip(myMousePos); + + if(!tip.empty()) + show(tip); + else + release(true); + } + } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -85,65 +104,40 @@ void ToolTip::hide() { myTimer = 0; myTipWidget = myFocusWidget = nullptr; - myTipShown = false; - myDialog.setDirtyChain(); + myDialog.instance().frameBuffer().setPendingRender(); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ToolTip::release() +void ToolTip::release(bool emptyTip) { if(myTipShown) { - myTimer = DELAY_TIME - 1; - myTipShown = false; - myDialog.setDirtyChain(); + myDialog.instance().frameBuffer().setPendingRender(); } // After displaying a tip, slowly reset the timer to 0 // until a new tip is requested - if(myTipWidget != myFocusWidget && myTimer) + if((emptyTip || myTipWidget != myFocusWidget) && myTimer) myTimer--; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ToolTip::request() +void ToolTip::show(const string& tip) { - myTipWidget = myFocusWidget; - - if(myTimer == DELAY_TIME) - show(); - - myTimer++; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ToolTip::show() -{ - if(myTipWidget == nullptr) - return; + myTipPos = myMousePos; const uInt32 V_GAP = 1; const uInt32 H_CURSOR = 18; - string text = myTipWidget->getToolTip(myPos); - - myTipShown = true; - if(text.empty()) - { - release(); - return; - } - - uInt32 width = std::min(myWidth, myFont->getStringWidth(text) + myTextXOfs * 2); - //uInt32 height = std::min(myHeight, font.getFontHeight() + myTextYOfs * 2); + uInt32 width = std::min(myWidth, myFont->getStringWidth(tip) + myTextXOfs * 2); // Note: The rects include HiDPI scaling const Common::Rect imageRect = myDialog.instance().frameBuffer().imageRect(); const Common::Rect dialogRect = myDialog.surface().dstRect(); // Limit position to app size and adjust accordingly - const Int32 xAbs = myPos.x + dialogRect.x() / myScale; - const uInt32 yAbs = myPos.y + dialogRect.y() / myScale; + const Int32 xAbs = myTipPos.x + dialogRect.x() / myScale; + const uInt32 yAbs = myTipPos.y + dialogRect.y() / myScale; Int32 x = std::min(xAbs, Int32(imageRect.w() / myScale - width)); const uInt32 y = (yAbs + myHeight + H_CURSOR > imageRect.h() / myScale) ? yAbs - myHeight - V_GAP @@ -161,11 +155,11 @@ void ToolTip::show() mySurface->frameRect(0, 0, width, myHeight, kColor); mySurface->fillRect(1, 1, width - 2, myHeight - 2, kWidColor); - mySurface->drawString(*myFont, text, myTextXOfs, myTextYOfs, + mySurface->drawString(*myFont, tip, myTextXOfs, myTextYOfs, width - myTextXOfs * 2, myHeight - myTextYOfs * 2, kTextColor); myTipShown = true; - myDialog.setDirtyChain(); + myDialog.instance().frameBuffer().setPendingRender(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx index 17ff2e7ec..2c9f95037 100644 --- a/src/gui/ToolTip.hxx +++ b/src/gui/ToolTip.hxx @@ -56,7 +56,7 @@ class ToolTip Hide a displayed tooltip and reset the timer slowly. This allows faster tip display of the next tip. */ - void release(); + void release(bool emptyTip); /** Update focused widget and current mouse position. @@ -69,10 +69,13 @@ class ToolTip void render(); private: - void show(); + void show(const string& tip); private: - static constexpr uInt32 DELAY_TIME = 45; // display delay + static constexpr uInt32 DELAY_TIME = 45; // display delay [frames] + // Tips are slower released than requested, so that repeated tips are shown + // faster. This constant defines how much faster. + static constexpr uInt32 RELEASE_SPEED = 2; Dialog& myDialog; const GUI::Font* myFont{nullptr}; @@ -80,7 +83,8 @@ class ToolTip const Widget* myFocusWidget{nullptr}; uInt32 myTimer{0}; - Common::Point myPos; + Common::Point myMousePos; + Common::Point myTipPos; uInt32 myWidth{0}; uInt32 myHeight{0}; uInt32 myTextXOfs{0}; diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index cfb933965..23c72760d 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -97,8 +97,8 @@ void Widget::draw() if(isDirty()) { - //cerr << " *** draw widget " << typeid(*this).name() << " ***" << endl; - cerr << "w"; + cerr << " *** draw widget " << typeid(*this).name() << " ***" << endl; + //cerr << "w"; FBSurface& s = _boss->dialog().surface(); @@ -418,6 +418,23 @@ void StaticTextWidget::setLabel(const string& label) setDirty(); } + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void StaticTextWidget::handleMouseEntered() +{ + if(isEnabled()) + // Mouse focus for tooltips must not change dirty status + setFlags(Widget::FLAG_MOUSE_FOCUS, false); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void StaticTextWidget::handleMouseLeft() +{ + if(isEnabled()) + // Mouse focus for tooltips must not change dirty status + clearFlags(Widget::FLAG_MOUSE_FOCUS, false); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StaticTextWidget::drawWidget(bool hilite) { diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 45c676fe2..c5eb233b3 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -186,8 +186,8 @@ class StaticTextWidget : public Widget ColorId shadowColor = kNone); ~StaticTextWidget() override = default; - void handleMouseEntered() override {} - void handleMouseLeft() override {} + void handleMouseEntered() override; + void handleMouseLeft() override; void setValue(int value); void setLabel(const string& label); From c19cde6f118ef3e4456fe51c0d0e19a4dc222626 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 18 Nov 2020 20:07:25 +0100 Subject: [PATCH 060/107] added multi-line tooltip support --- src/debugger/gui/RomListWidget.cxx | 6 ++-- src/emucore/FBSurface.cxx | 52 +++++++++++++++++------------- src/emucore/FBSurface.hxx | 15 +++++++-- src/gui/EditableWidget.cxx | 8 ++--- src/gui/EditableWidget.hxx | 2 +- src/gui/ToolTip.cxx | 44 +++++++++++++++++-------- src/gui/ToolTip.hxx | 6 +++- 7 files changed, 84 insertions(+), 49 deletions(-) diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 861a4112d..5f4487786 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -40,7 +40,7 @@ RomListWidget::RomListWidget(GuiObject* boss, const GUI::Font& lfont, _textcolorhi = kTextColor; _editMode = false; - _dyCaret = 1; + _dyText = -1; // fixes the vertical position of selected text _cols = w / _fontWidth; _rows = h / _lineHeight; @@ -631,8 +631,8 @@ Common::Rect RomListWidget::getEditRect() const { const int yoffset = std::max(0, (_selectedItem - _currentPos) * _lineHeight); - return Common::Rect(2 + _w - _bytesWidth, 1 + yoffset, - _w, _lineHeight + yoffset); + return Common::Rect(2 + _w - _bytesWidth, 1 + yoffset + 1, + _w, _lineHeight + yoffset + 1); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/emucore/FBSurface.cxx b/src/emucore/FBSurface.cxx index c489fc703..cfcf58ff3 100644 --- a/src/emucore/FBSurface.cxx +++ b/src/emucore/FBSurface.cxx @@ -296,21 +296,39 @@ void FBSurface::frameRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void FBSurface::wrapString(const string& inStr, int pos, string& leftStr, string& rightStr) const +void FBSurface::splitString(const GUI::Font& font, const string& s, int w, + string& left, string& right) const { - for(int i = pos; i > 0; --i) + uInt32 pos; + int w2 = 0; + bool split = false; + + // SLOW algorithm to find the acceptable length. But it is good enough for now. + for(pos = 0; pos < s.size(); ++pos) { - if(isWhiteSpace(inStr[i])) + int charWidth = font.getCharWidth(s[pos]); + if(w2 + charWidth > w) { - leftStr = inStr.substr(0, i); - if(inStr[i] == ' ') // skip leading space after line break - i++; - rightStr = inStr.substr(i); - return; + split = true; + break; } + w2 += charWidth; } - leftStr = inStr.substr(0, pos); - rightStr = inStr.substr(pos); + + if(split) + for(int i = pos; i > 0; --i) + { + if(isWhiteSpace(s[i])) + { + left = s.substr(0, i); + if(s[i] == ' ') // skip leading space after line break + i++; + right = s.substr(i); + return; + } + } + left = s.substr(0, pos); + right = s.substr(pos); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -340,21 +358,9 @@ int FBSurface::drawString(const GUI::Font& font, const string& s, while (font.getStringWidth(inStr) > w && h >= font.getFontHeight() * 2) { // String is too wide. - uInt32 i; string leftStr, rightStr; - int w2 = 0; - // SLOW algorithm to find the acceptable length. But it is good enough for now. - for(i = 0; i < inStr.size(); ++i) - { - int charWidth = font.getCharWidth(inStr[i]); - if(w2 + charWidth > w) - break; - - w2 += charWidth; - //str += inStr[i]; - } - wrapString(inStr, i, leftStr, rightStr); + splitString(font, inStr, w, leftStr, rightStr); drawString(font, leftStr, x, y, w, color, align, deltax, false, shadowColor); h -= font.getFontHeight(); y += font.getFontHeight(); diff --git a/src/emucore/FBSurface.hxx b/src/emucore/FBSurface.hxx index 71bddc289..de19fca28 100644 --- a/src/emucore/FBSurface.hxx +++ b/src/emucore/FBSurface.hxx @@ -247,6 +247,18 @@ class FBSurface ColorId color, TextAlign align = TextAlign::Left, int deltax = 0, bool useEllipsis = true, ColorId shadowColor = kNone); + /** + Splits a given string to a given width considering whitespaces. + + @param font The font to draw the string with + @param s The string to split + @param w The width of the string area + @param left The left part of the split string + @param right The right part of the split string + */ + void splitString(const GUI::Font& font, const string& s, int w, + string& left, string& right) const; + /** The rendering attributes that can be modified for this texture. These probably can only be implemented in child FBSurfaces where @@ -383,9 +395,6 @@ class FBSurface */ bool checkBounds(const uInt32 x, const uInt32 y) const; - void wrapString(const string& inStr, int pos, - string& leftStr, string& rightStr) const; - /** Check if the given character is a whitespace. @param s Character to check diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index 8ff411f83..e015dd899 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -379,8 +379,8 @@ void EditableWidget::drawCaretSelection() x += _x; y += _y; - s.fillRect(x - 1, y + 1 + _dyCaret, w + 1, h - 3, kTextColorHi); - s.drawString(_font, text, x, y + 1, w, h, + s.fillRect(x - 1, y + 1, w + 1, h - 3, kTextColorHi); + s.drawString(_font, text, x, y + 1 + _dyText, w, h, kTextColorInv, TextAlign::Left, 0, false); } @@ -397,8 +397,8 @@ void EditableWidget::drawCaretSelection() x += _x; y += _y; - s.vLine(x, y + 1 + _dyCaret, y + editRect.h() - 3, color); - s.vLine(x - 1, y + 1 + _dyCaret, y + editRect.h() - 3, color); + s.vLine(x, y + 1, y + editRect.h() - 3, color); + s.vLine(x - 1, y + 1, y + editRect.h() - 3, color); clearDirty(); } } diff --git a/src/gui/EditableWidget.hxx b/src/gui/EditableWidget.hxx index 497c2a4e8..3ffe09a65 100644 --- a/src/gui/EditableWidget.hxx +++ b/src/gui/EditableWidget.hxx @@ -125,7 +125,7 @@ class EditableWidget : public Widget, public CommandSender protected: int _editScrollOffset{0}; bool _editMode{true}; - int _dyCaret{0}; + int _dyText{0}; private: TextFilter _filter; diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 5d37478db..5cfed635c 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -27,7 +27,6 @@ // TODOs: // - option to disable tips -// - multi line tips // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ToolTip::ToolTip(Dialog& dialog, const GUI::Font& font) @@ -47,8 +46,8 @@ void ToolTip::setFont(const GUI::Font& font) myTextXOfs = fontHeight < 24 ? 5 : 8; myTextYOfs = fontHeight < 24 ? 2 : 3; - myWidth = fontWidth * MAX_LEN + myTextXOfs * 2; - myHeight = fontHeight + myTextYOfs * 2; + myWidth = fontWidth * MAX_COLUMNS + myTextXOfs * 2; + myHeight = fontHeight * MAX_ROWS + myTextYOfs * 2; mySurface = myDialog.instance().frameBuffer().allocateSurface(myWidth, myHeight); } @@ -129,9 +128,30 @@ void ToolTip::show(const string& tip) { myTipPos = myMousePos; + uInt32 maxWidth = std::min(myWidth - myTextXOfs * 2, uInt32(myFont->getStringWidth(tip))); + + mySurface->fillRect(1, 1, maxWidth + myTextXOfs * 2 - 2, myHeight - 2, kWidColor); + int lines = std::min(MAX_ROWS, + uInt32(mySurface->drawString(*myFont, tip, myTextXOfs, myTextYOfs, + maxWidth, myHeight - myTextYOfs * 2, + kTextColor))); + // Calculate maximum width of drawn string lines + uInt32 width = 0; + string inStr = tip; + for(int i = 0; i < lines; ++i) + { + string leftStr, rightStr; + + mySurface->splitString(*myFont, inStr, maxWidth, leftStr, rightStr); + width = std::max(width, uInt32(myFont->getStringWidth(leftStr))); + inStr = rightStr; + } + width += myTextXOfs * 2; + + // Calculate and set surface size and position + const uInt32 height = std::min(myHeight, myFont->getFontHeight() * lines + myTextYOfs * 2); const uInt32 V_GAP = 1; const uInt32 H_CURSOR = 18; - uInt32 width = std::min(myWidth, myFont->getStringWidth(tip) + myTextXOfs * 2); // Note: The rects include HiDPI scaling const Common::Rect imageRect = myDialog.instance().frameBuffer().imageRect(); const Common::Rect dialogRect = myDialog.surface().dstRect(); @@ -139,24 +159,20 @@ void ToolTip::show(const string& tip) const Int32 xAbs = myTipPos.x + dialogRect.x() / myScale; const uInt32 yAbs = myTipPos.y + dialogRect.y() / myScale; Int32 x = std::min(xAbs, Int32(imageRect.w() / myScale - width)); - const uInt32 y = (yAbs + myHeight + H_CURSOR > imageRect.h() / myScale) - ? yAbs - myHeight - V_GAP + const uInt32 y = (yAbs + height + H_CURSOR > imageRect.h() / myScale) + ? yAbs - height - V_GAP : yAbs + H_CURSOR / myScale + V_GAP; if(x < 0) { x = 0; - width = imageRect.w() / myScale; + width = std::min(width, imageRect.w() / myScale); } - mySurface->setSrcSize(width, myHeight); - mySurface->setDstSize(width * myScale, myHeight * myScale); + mySurface->setSrcSize(width, height); + mySurface->setDstSize(width * myScale, height * myScale); mySurface->setDstPos(x * myScale, y * myScale); - - mySurface->frameRect(0, 0, width, myHeight, kColor); - mySurface->fillRect(1, 1, width - 2, myHeight - 2, kWidColor); - mySurface->drawString(*myFont, tip, myTextXOfs, myTextYOfs, - width - myTextXOfs * 2, myHeight - myTextYOfs * 2, kTextColor); + mySurface->frameRect(0, 0, width, height, kColor); myTipShown = true; myDialog.instance().frameBuffer().setPendingRender(); diff --git a/src/gui/ToolTip.hxx b/src/gui/ToolTip.hxx index 2c9f95037..71da6dc13 100644 --- a/src/gui/ToolTip.hxx +++ b/src/gui/ToolTip.hxx @@ -33,9 +33,13 @@ class FBSurface; class ToolTip { + private: + static constexpr uInt32 MAX_COLUMNS = 60; + static constexpr uInt32 MAX_ROWS = 5; + public: // Maximum tooltip length - static constexpr uInt32 MAX_LEN = 80; + static constexpr uInt32 MAX_LEN = MAX_COLUMNS * MAX_ROWS; ToolTip(Dialog& dialog, const GUI::Font& font); ~ToolTip() = default; From 59f157187fb3fceb62e61d1318247263bcd3de95 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 18 Nov 2020 21:02:42 +0100 Subject: [PATCH 061/107] improved string wrapping (incl. '\n') fixed potential exception in StringListWidget --- src/emucore/FBSurface.cxx | 17 +++++++++++------ src/gui/StringListWidget.cxx | 14 ++++++++++++-- src/gui/ToolTip.cxx | 3 --- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/emucore/FBSurface.cxx b/src/emucore/FBSurface.cxx index cfcf58ff3..6421dd171 100644 --- a/src/emucore/FBSurface.cxx +++ b/src/emucore/FBSurface.cxx @@ -307,7 +307,7 @@ void FBSurface::splitString(const GUI::Font& font, const string& s, int w, for(pos = 0; pos < s.size(); ++pos) { int charWidth = font.getCharWidth(s[pos]); - if(w2 + charWidth > w) + if(w2 + charWidth > w || s[pos] == '\n') { split = true; break; @@ -321,7 +321,7 @@ void FBSurface::splitString(const GUI::Font& font, const string& s, int w, if(isWhiteSpace(s[i])) { left = s.substr(0, i); - if(s[i] == ' ') // skip leading space after line break + if(s[i] == ' ' || s[pos] == '\n') // skip leading space after line break i++; right = s.substr(i); return; @@ -334,7 +334,7 @@ void FBSurface::splitString(const GUI::Font& font, const string& s, int w, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool FBSurface::isWhiteSpace(const char s) const { - const string WHITESPACES = " ,.;:+-"; + const string WHITESPACES = " ,.;:+-*/'([\n"; for(size_t i = 0; i < WHITESPACES.length(); ++i) if(s == WHITESPACES[i]) @@ -349,13 +349,14 @@ int FBSurface::drawString(const GUI::Font& font, const string& s, ColorId color, TextAlign align, int deltax, bool useEllipsis, ColorId shadowColor) { - int lines = 1; + int lines = 0; #ifdef GUI_SUPPORT string inStr = s; // draw multiline string - while (font.getStringWidth(inStr) > w && h >= font.getFontHeight() * 2) + //while (font.getStringWidth(inStr) > w && h >= font.getFontHeight() * 2) + while(inStr.length() && h >= font.getFontHeight() * 2) { // String is too wide. string leftStr, rightStr; @@ -367,7 +368,11 @@ int FBSurface::drawString(const GUI::Font& font, const string& s, inStr = rightStr; lines++; } - drawString(font, inStr, x, y, w, color, align, deltax, useEllipsis, shadowColor); + if(inStr.length()) + { + drawString(font, inStr, x, y, w, color, align, deltax, useEllipsis, shadowColor); + lines++; + } #endif return lines; } diff --git a/src/gui/StringListWidget.cxx b/src/gui/StringListWidget.cxx index 74cf23e40..04820090c 100644 --- a/src/gui/StringListWidget.cxx +++ b/src/gui/StringListWidget.cxx @@ -53,14 +53,24 @@ void StringListWidget::setList(const StringList& list) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int StringListWidget::getToolTipIndex(Common::Point pos) const { - return (pos.y - getAbsY()) / _lineHeight + _currentPos; + int idx = (pos.y - getAbsY()) / _lineHeight + _currentPos; + + if(idx >= int(_list.size())) + return -1; + else + return idx; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - string StringListWidget::getToolTip(Common::Point pos) const { Common::Rect rect = getEditRect(); - const string value = _list[getToolTipIndex(pos)]; + int idx = getToolTipIndex(pos); + + if(idx < 0) + return EmptyString; + + const string value = _list[idx]; if(uInt32(_font.getStringWidth(value)) > rect.w()) return _toolTipText + value; diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 5cfed635c..6e6ef9fc9 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -25,9 +25,6 @@ #include "ToolTip.hxx" -// TODOs: -// - option to disable tips - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ToolTip::ToolTip(Dialog& dialog, const GUI::Font& font) : myDialog(dialog) From 355dc9597d597048b637e9483424491dd24b0e9a Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Wed, 18 Nov 2020 17:56:57 -0330 Subject: [PATCH 062/107] Use const references where appropriate. Probably not a huge performance gain, but added to match the rest of the code. --- src/debugger/gui/DataGridWidget.cxx | 7 ++++--- src/debugger/gui/DataGridWidget.hxx | 6 +++--- src/debugger/gui/RomListWidget.cxx | 9 +++++---- src/debugger/gui/RomListWidget.hxx | 6 +++--- src/debugger/gui/ToggleWidget.cxx | 10 ++++------ src/debugger/gui/ToggleWidget.hxx | 6 +++--- src/gui/StringListWidget.cxx | 7 ++++--- src/gui/StringListWidget.hxx | 6 +++--- src/gui/Widget.hxx | 5 +++-- 9 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index 9e75a955f..fb078ba18 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -571,7 +571,7 @@ void DataGridWidget::handleCommand(CommandSender* sender, int cmd, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -int DataGridWidget::getToolTipIndex(Common::Point pos) const +int DataGridWidget::getToolTipIndex(const Common::Point& pos) const { const int col = (pos.x - getAbsX()) / _colWidth; const int row = (pos.y - getAbsY()) / _rowHeight; @@ -580,7 +580,7 @@ int DataGridWidget::getToolTipIndex(Common::Point pos) const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -string DataGridWidget::getToolTip(Common::Point pos) const +string DataGridWidget::getToolTip(const Common::Point& pos) const { const Int32 val = _valueList[getToolTipIndex(pos)]; ostringstream buf; @@ -595,7 +595,8 @@ string DataGridWidget::getToolTip(Common::Point pos) const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool DataGridWidget::changedToolTip(Common::Point oldPos, Common::Point newPos) const +bool DataGridWidget::changedToolTip(const Common::Point& oldPos, + const Common::Point& newPos) const { return getToolTipIndex(oldPos) != getToolTipIndex(newPos); } diff --git a/src/debugger/gui/DataGridWidget.hxx b/src/debugger/gui/DataGridWidget.hxx index 1a402659d..37434db3c 100644 --- a/src/debugger/gui/DataGridWidget.hxx +++ b/src/debugger/gui/DataGridWidget.hxx @@ -84,8 +84,8 @@ class DataGridWidget : public EditableWidget void setCrossed(bool enable) { _crossGrid = enable; } - string getToolTip(Common::Point pos) const override; - bool changedToolTip(Common::Point oldPos, Common::Point newPos) const override; + string getToolTip(const Common::Point& pos) const override; + bool changedToolTip(const Common::Point& oldPos, const Common::Point& newPos) const override; protected: void drawWidget(bool hilite) override; @@ -150,7 +150,7 @@ class DataGridWidget : public EditableWidget void enableEditMode(bool state) { _editMode = state; } - int getToolTipIndex(Common::Point pos) const; + int getToolTipIndex(const Common::Point& pos) const; private: // Following constructors and assignment operators not supported diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 5f4487786..236f99a31 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -444,7 +444,7 @@ void RomListWidget::lostFocusWidget() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Common::Point RomListWidget::getToolTipIndex(Common::Point pos) const +Common::Point RomListWidget::getToolTipIndex(const Common::Point& pos) const { const Common::Rect& r = getEditRect(); const int col = (pos.x - r.x() - getAbsX()) / _font.getMaxCharWidth(); @@ -457,9 +457,9 @@ Common::Point RomListWidget::getToolTipIndex(Common::Point pos) const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -string RomListWidget::getToolTip(Common::Point pos) const +string RomListWidget::getToolTip(const Common::Point& pos) const { - const Common::Point idx = getToolTipIndex(pos); + const Common::Point& idx = getToolTipIndex(pos); if(idx.y == -1) return EmptyString; @@ -499,7 +499,8 @@ string RomListWidget::getToolTip(Common::Point pos) const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool RomListWidget::changedToolTip(Common::Point oldPos, Common::Point newPos) const +bool RomListWidget::changedToolTip(const Common::Point& oldPos, + const Common::Point& newPos) const { return getToolTipIndex(oldPos) != getToolTipIndex(newPos); } diff --git a/src/debugger/gui/RomListWidget.hxx b/src/debugger/gui/RomListWidget.hxx index 3f26d217f..9baa9350a 100644 --- a/src/debugger/gui/RomListWidget.hxx +++ b/src/debugger/gui/RomListWidget.hxx @@ -56,8 +56,8 @@ class RomListWidget : public EditableWidget void setSelected(int item); void setHighlighted(int item); - string getToolTip(Common::Point pos) const override; - bool changedToolTip(Common::Point oldPos, Common::Point newPos) const override; + string getToolTip(const Common::Point& pos) const override; + bool changedToolTip(const Common::Point& oldPos, const Common::Point& newPos) const override; protected: void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; @@ -88,7 +88,7 @@ class RomListWidget : public EditableWidget private: void scrollToCurrent(int item); - Common::Point getToolTipIndex(Common::Point pos) const; + Common::Point getToolTipIndex(const Common::Point& pos) const; private: unique_ptr myMenu; diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index 2a4187c6d..4fced1146 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -205,7 +205,7 @@ void ToggleWidget::handleCommand(CommandSender* sender, int cmd, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -int ToggleWidget::getToolTipIndex(Common::Point pos) const +int ToggleWidget::getToolTipIndex(const Common::Point& pos) const { const int row = (pos.y - getAbsY()) / _rowHeight; @@ -213,7 +213,7 @@ int ToggleWidget::getToolTipIndex(Common::Point pos) const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -string ToggleWidget::getToolTip(Common::Point pos) const +string ToggleWidget::getToolTip(const Common::Point& pos) const { const int idx = getToolTipIndex(pos); Int32 val = 0; @@ -243,10 +243,8 @@ string ToggleWidget::getToolTip(Common::Point pos) const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool ToggleWidget::changedToolTip(Common::Point oldPos, Common::Point newPos) const +bool ToggleWidget::changedToolTip(const Common::Point& oldPos, + const Common::Point& newPos) const { return getToolTipIndex(oldPos) != getToolTipIndex(newPos); } - - - diff --git a/src/debugger/gui/ToggleWidget.hxx b/src/debugger/gui/ToggleWidget.hxx index 3f74237b0..347359677 100644 --- a/src/debugger/gui/ToggleWidget.hxx +++ b/src/debugger/gui/ToggleWidget.hxx @@ -46,8 +46,8 @@ class ToggleWidget : public Widget, public CommandSender void setEditable(bool editable) { _editable = editable; } bool isEditable() const { return _editable; } - string getToolTip(Common::Point pos) const override; - bool changedToolTip(Common::Point oldPos, Common::Point newPos) const override; + string getToolTip(const Common::Point& pos) const override; + bool changedToolTip(const Common::Point& oldPos, const Common::Point& newPos) const override; protected: bool hasToolTip() const override { return true; } @@ -76,7 +76,7 @@ class ToggleWidget : public Widget, public CommandSender bool handleKeyDown(StellaKey key, StellaMod mod) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override; - int getToolTipIndex(Common::Point pos) const; + int getToolTipIndex(const Common::Point& pos) const; // Following constructors and assignment operators not supported ToggleWidget() = delete; diff --git a/src/gui/StringListWidget.cxx b/src/gui/StringListWidget.cxx index 04820090c..a512425b5 100644 --- a/src/gui/StringListWidget.cxx +++ b/src/gui/StringListWidget.cxx @@ -51,7 +51,7 @@ void StringListWidget::setList(const StringList& list) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -int StringListWidget::getToolTipIndex(Common::Point pos) const +int StringListWidget::getToolTipIndex(const Common::Point& pos) const { int idx = (pos.y - getAbsY()) / _lineHeight + _currentPos; @@ -62,7 +62,7 @@ int StringListWidget::getToolTipIndex(Common::Point pos) const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -string StringListWidget::getToolTip(Common::Point pos) const +string StringListWidget::getToolTip(const Common::Point& pos) const { Common::Rect rect = getEditRect(); int idx = getToolTipIndex(pos); @@ -79,7 +79,8 @@ string StringListWidget::getToolTip(Common::Point pos) const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool StringListWidget::changedToolTip(Common::Point oldPos, Common::Point newPos) const +bool StringListWidget::changedToolTip(const Common::Point& oldPos, + const Common::Point& newPos) const { bool ch = getToolTipIndex(oldPos) != getToolTipIndex(newPos) && getToolTip(oldPos) != getToolTip(newPos); diff --git a/src/gui/StringListWidget.hxx b/src/gui/StringListWidget.hxx index fb96a9e22..f0bf8544b 100644 --- a/src/gui/StringListWidget.hxx +++ b/src/gui/StringListWidget.hxx @@ -32,8 +32,8 @@ class StringListWidget : public ListWidget void setList(const StringList& list); bool wantsFocus() const override { return true; } - string getToolTip(Common::Point pos) const override; - bool changedToolTip(Common::Point oldPos, Common::Point newPos) const override; + string getToolTip(const Common::Point& pos) const override; + bool changedToolTip(const Common::Point& oldPos, const Common::Point& newPos) const override; protected: // display depends on _hasFocus so we have to redraw when focus changes @@ -50,7 +50,7 @@ class StringListWidget : public ListWidget int _textOfs{0}; private: - int getToolTipIndex(Common::Point pos) const; + int getToolTipIndex(const Common::Point& pos) const; private: // Following constructors and assignment operators not supported diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index c5eb233b3..64685813a 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -106,8 +106,9 @@ class Widget : public GuiObject void setShadowColor(ColorId color) { _shadowcolor = color; setDirty(); } void setToolTip(const string& text); - virtual string getToolTip(Common::Point pos) const { return _toolTipText; } - virtual bool changedToolTip(Common::Point oldPos, Common::Point newPos) const { return false; } + virtual string getToolTip(const Common::Point& pos) const { return _toolTipText; } + virtual bool changedToolTip(const Common::Point& oldPos, + const Common::Point& newPos) const { return false; } virtual void loadConfig() { } From d0e818a693c32f926b17a123070f3702b782dd54 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 18 Nov 2020 23:52:19 +0100 Subject: [PATCH 063/107] added a few more tooltips to VideoAudioDialog --- src/gui/VideoAudioDialog.cxx | 8 ++++++++ src/gui/VideoAudioDialog.hxx | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/gui/VideoAudioDialog.cxx b/src/gui/VideoAudioDialog.cxx index def965aa6..ac2ad19bf 100644 --- a/src/gui/VideoAudioDialog.cxx +++ b/src/gui/VideoAudioDialog.cxx @@ -237,6 +237,7 @@ void VideoAudioDialog::addPaletteTab() myPhaseShiftNtsc->setMinValue((PaletteHandler::DEF_NTSC_SHIFT - PaletteHandler::MAX_PHASE_SHIFT) * 10); myPhaseShiftNtsc->setMaxValue((PaletteHandler::DEF_NTSC_SHIFT + PaletteHandler::MAX_PHASE_SHIFT) * 10); myPhaseShiftNtsc->setTickmarkIntervals(4); + myPhaseShiftNtsc->setToolTip("Adjust NTSC phase shift of 'Custom' palette."); wid.push_back(myPhaseShiftNtsc); ypos += lineHeight + VGAP; @@ -246,6 +247,7 @@ void VideoAudioDialog::addPaletteTab() myPhaseShiftPal->setMinValue((PaletteHandler::DEF_PAL_SHIFT - PaletteHandler::MAX_PHASE_SHIFT) * 10); myPhaseShiftPal->setMaxValue((PaletteHandler::DEF_PAL_SHIFT + PaletteHandler::MAX_PHASE_SHIFT) * 10); myPhaseShiftPal->setTickmarkIntervals(4); + myPhaseShiftPal->setToolTip("Adjust PAL phase shift of 'Custom' palette."); wid.push_back(myPhaseShiftPal); ypos += lineHeight + VGAP; @@ -258,6 +260,7 @@ void VideoAudioDialog::addPaletteTab() myTVRedScale->setMinValue(0); myTVRedScale->setMaxValue(100); myTVRedScale->setTickmarkIntervals(2); + myTVRedScale->setToolTip("Adjust red saturation of 'Custom' palette."); wid.push_back(myTVRedScale); const int xposr = myTIAPalette->getRight() - rgbsWidth; @@ -267,6 +270,7 @@ void VideoAudioDialog::addPaletteTab() myTVRedShift->setMinValue((PaletteHandler::DEF_RGB_SHIFT - PaletteHandler::MAX_RGB_SHIFT) * 10); myTVRedShift->setMaxValue((PaletteHandler::DEF_RGB_SHIFT + PaletteHandler::MAX_RGB_SHIFT) * 10); myTVRedShift->setTickmarkIntervals(2); + myTVRedShift->setToolTip("Adjust red shift of 'Custom' palette."); wid.push_back(myTVRedShift); ypos += lineHeight + VGAP; @@ -276,6 +280,7 @@ void VideoAudioDialog::addPaletteTab() myTVGreenScale->setMinValue(0); myTVGreenScale->setMaxValue(100); myTVGreenScale->setTickmarkIntervals(2); + myTVGreenScale->setToolTip("Adjust green saturation of 'Custom' palette."); wid.push_back(myTVGreenScale); myTVGreenShift = @@ -284,6 +289,7 @@ void VideoAudioDialog::addPaletteTab() myTVGreenShift->setMinValue((PaletteHandler::DEF_RGB_SHIFT - PaletteHandler::MAX_RGB_SHIFT) * 10); myTVGreenShift->setMaxValue((PaletteHandler::DEF_RGB_SHIFT + PaletteHandler::MAX_RGB_SHIFT) * 10); myTVGreenShift->setTickmarkIntervals(2); + myTVGreenShift->setToolTip("Adjust green shift of 'Custom' palette."); wid.push_back(myTVGreenShift); ypos += lineHeight + VGAP; @@ -293,6 +299,7 @@ void VideoAudioDialog::addPaletteTab() myTVBlueScale->setMinValue(0); myTVBlueScale->setMaxValue(100); myTVBlueScale->setTickmarkIntervals(2); + myTVBlueScale->setToolTip("Adjust blue saturation of 'Custom' palette."); wid.push_back(myTVBlueScale); myTVBlueShift = @@ -301,6 +308,7 @@ void VideoAudioDialog::addPaletteTab() myTVBlueShift->setMinValue((PaletteHandler::DEF_RGB_SHIFT - PaletteHandler::MAX_RGB_SHIFT) * 10); myTVBlueShift->setMaxValue((PaletteHandler::DEF_RGB_SHIFT + PaletteHandler::MAX_RGB_SHIFT) * 10); myTVBlueShift->setTickmarkIntervals(2); + myTVBlueShift->setToolTip("Adjust blue shift of 'Custom' palette."); wid.push_back(myTVBlueShift); ypos += lineHeight + VGAP; xpos -= INDENT; diff --git a/src/gui/VideoAudioDialog.hxx b/src/gui/VideoAudioDialog.hxx index 1d0e8a637..0c8c44bf7 100644 --- a/src/gui/VideoAudioDialog.hxx +++ b/src/gui/VideoAudioDialog.hxx @@ -16,7 +16,7 @@ //============================================================================ #ifndef VIDEOAUDIO_DIALOG_HXX -#define VIDEOAUDIO_DIALOG_HXX +#define D class CommandSender; class CheckboxWidget; From 9ab2a5c41745db578f4cd5fe4a89da37b93bc12a Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 18 Nov 2020 23:54:43 +0100 Subject: [PATCH 064/107] disable tooltip when displayed value changes/is edited --- src/debugger/gui/DataGridWidget.cxx | 6 +++++- src/debugger/gui/RomListWidget.cxx | 3 +++ src/debugger/gui/ToggleWidget.cxx | 4 ++++ src/gui/StringListWidget.cxx | 4 ++-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index fb078ba18..4304ecb0e 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -17,6 +17,7 @@ #include "Widget.hxx" #include "Dialog.hxx" +#include "ToolTip.hxx" #include "Font.hxx" #include "OSystem.hxx" #include "Debugger.hxx" @@ -294,6 +295,7 @@ void DataGridWidget::handleMouseWheel(int x, int y, int direction) else if(direction < 0) incrementCell(); } + dialog().tooltip().hide(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -491,6 +493,7 @@ bool DataGridWidget::handleKeyDown(StellaKey key, StellaMod mod) sendCommand(DataGridWidget::kSelectionChangedCmd, _selectedItem, _id); setDirty(); + dialog().tooltip().hide(); } _currentKeyDown = key; @@ -582,7 +585,8 @@ int DataGridWidget::getToolTipIndex(const Common::Point& pos) const // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - string DataGridWidget::getToolTip(const Common::Point& pos) const { - const Int32 val = _valueList[getToolTipIndex(pos)]; + const int idx = getToolTipIndex(pos); + const Int32 val = _valueList[idx]; ostringstream buf; buf << _toolTipText diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 236f99a31..c06675e32 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -20,6 +20,8 @@ #include "Debugger.hxx" #include "DiStella.hxx" #include "Widget.hxx" +#include "Dialog.hxx" +#include "ToolTip.hxx" #include "StellaKeys.hxx" #include "FBSurface.hxx" #include "Font.hxx" @@ -646,6 +648,7 @@ void RomListWidget::startEditMode() return; _editMode = true; + dialog().tooltip().hide(); switch(myDisasm->list[_selectedItem].type) { case Device::GFX: diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index 4fced1146..ac321f4bf 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -19,6 +19,8 @@ #include "Base.hxx" #include "StellaKeys.hxx" #include "Widget.hxx" +#include "Dialog.hxx" +#include "ToolTip.hxx" #include "ToggleWidget.hxx" // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -51,6 +53,7 @@ void ToggleWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) _selectedItem = newSelectedItem; _currentRow = _selectedItem / _cols; _currentCol = _selectedItem - (_currentRow * _cols); + dialog().tooltip().hide(); setDirty(); } } @@ -182,6 +185,7 @@ bool ToggleWidget::handleKeyDown(StellaKey key, StellaMod mod) _stateList[_selectedItem] = !_stateList[_selectedItem]; _changedList[_selectedItem] = !_changedList[_selectedItem]; sendCommand(ToggleWidget::kItemDataChangedCmd, _selectedItem, _id); + dialog().tooltip().hide(); } setDirty(); diff --git a/src/gui/StringListWidget.cxx b/src/gui/StringListWidget.cxx index a512425b5..536240b8a 100644 --- a/src/gui/StringListWidget.cxx +++ b/src/gui/StringListWidget.cxx @@ -56,9 +56,9 @@ int StringListWidget::getToolTipIndex(const Common::Point& pos) const int idx = (pos.y - getAbsY()) / _lineHeight + _currentPos; if(idx >= int(_list.size())) - return -1; + return -1; else - return idx; + return idx; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From db8e6d3ea8e1f0b6b9fae226fe5d6f25505c1142 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Wed, 18 Nov 2020 22:18:07 -0330 Subject: [PATCH 065/107] Fix some clang warnings and minor typo. --- src/debugger/gui/Cart3EWidget.cxx | 2 +- src/debugger/gui/RomListWidget.cxx | 2 +- src/gui/VideoAudioDialog.hxx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/debugger/gui/Cart3EWidget.cxx b/src/debugger/gui/Cart3EWidget.cxx index 910f03368..9e0822b51 100644 --- a/src/debugger/gui/Cart3EWidget.cxx +++ b/src/debugger/gui/Cart3EWidget.cxx @@ -117,7 +117,7 @@ void Cartridge3EWidget::loadConfig() myBankWidgets[1]->setSelectedIndex(bank - myCart.romBankCount(), oldBank != bank); } - CartDebugWidget::loadConfig(); + CartDebugWidget::loadConfig(); // Intentionally calling grand-parent method } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index c06675e32..868b72cdf 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -468,7 +468,7 @@ string RomListWidget::getToolTip(const Common::Point& pos) const const string bytes = myDisasm->list[idx.y].bytes; - if(bytes.length() < size_t(idx.x + 1)) + if(static_cast(bytes.length()) < idx.x + 1) return EmptyString; Int32 val; diff --git a/src/gui/VideoAudioDialog.hxx b/src/gui/VideoAudioDialog.hxx index 0c8c44bf7..550617d2d 100644 --- a/src/gui/VideoAudioDialog.hxx +++ b/src/gui/VideoAudioDialog.hxx @@ -16,7 +16,7 @@ //============================================================================ #ifndef VIDEOAUDIO_DIALOG_HXX -#define D +#define VIDEOAUDIO_DIALOG_HXX class CommandSender; class CheckboxWidget; @@ -38,7 +38,7 @@ class VideoAudioDialog : public Dialog { public: VideoAudioDialog(OSystem& osystem, DialogContainer& parent, const GUI::Font& font, - int max_w, int max_h); + int max_w, int max_h); ~VideoAudioDialog() override = default; private: From 2bdd09fec99e0743d85549973af33ebfed1b7889 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 19 Nov 2020 12:25:07 +0100 Subject: [PATCH 066/107] added tooltip display of RAM labels in DataGridWidgets added tooltip display of labels of some ToggleBitsWidget bits merged tooltip display of 2nd and 3rd RomListWidget byte added tooltip display of signed values --- src/debugger/gui/CartRamWidget.hxx | 2 +- src/debugger/gui/DataGridWidget.cxx | 13 ++++++++++- src/debugger/gui/DataGridWidget.hxx | 2 +- src/debugger/gui/RamWidget.cxx | 6 ++--- src/debugger/gui/RamWidget.hxx | 6 +++-- src/debugger/gui/RiotRamWidget.hxx | 3 ++- src/debugger/gui/RiotWidget.cxx | 26 +++++++++++++++++++-- src/debugger/gui/RomListWidget.cxx | 23 ++++++++++++++----- src/debugger/gui/ToggleBitWidget.cxx | 34 ++++++++++++++++++++++++++-- src/debugger/gui/ToggleBitWidget.hxx | 6 +++++ src/debugger/gui/ToggleWidget.cxx | 14 +++++++++--- src/debugger/gui/ToggleWidget.hxx | 3 +-- src/debugger/gui/module.mk | 1 + src/windows/Stella.vcxproj | 6 +++++ src/windows/Stella.vcxproj.filters | 6 +++++ 15 files changed, 127 insertions(+), 24 deletions(-) diff --git a/src/debugger/gui/CartRamWidget.hxx b/src/debugger/gui/CartRamWidget.hxx index bc0f4f810..d24a5d134 100644 --- a/src/debugger/gui/CartRamWidget.hxx +++ b/src/debugger/gui/CartRamWidget.hxx @@ -67,11 +67,11 @@ class CartRamWidget : public Widget, public CommandSender int x, int y, int w, int h, CartDebugWidget& cartDebug); ~InternalRamWidget() override = default; + string getLabel(int addr) const override; private: uInt8 getValue(int addr) const override; void setValue(int addr, uInt8 value) override; - string getLabel(int addr) const override; void fillList(uInt32 start, uInt32 size, IntArray& alist, IntArray& vlist, BoolArray& changed) const override; diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index 4304ecb0e..5d0dc0623 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -579,13 +579,20 @@ int DataGridWidget::getToolTipIndex(const Common::Point& pos) const const int col = (pos.x - getAbsX()) / _colWidth; const int row = (pos.y - getAbsY()) / _rowHeight; - return row * _cols + col; + if(row >= 0 && row < _rows && col >= 0 && col < _cols) + return row * _cols + col; + else + return -1; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - string DataGridWidget::getToolTip(const Common::Point& pos) const { const int idx = getToolTipIndex(pos); + + if(idx < 0) + return EmptyString; + const Int32 val = _valueList[idx]; ostringstream buf; @@ -593,7 +600,11 @@ string DataGridWidget::getToolTip(const Common::Point& pos) const << "$" << Common::Base::toString(val, Common::Base::Fmt::_16) << " = #" << val; if(val < 0x100) + { + if(val >= 0x80) + buf << '/' << -(0x100 - val); buf << " = %" << Common::Base::toString(val, Common::Base::Fmt::_2); + } return buf.str(); } diff --git a/src/debugger/gui/DataGridWidget.hxx b/src/debugger/gui/DataGridWidget.hxx index 37434db3c..848993311 100644 --- a/src/debugger/gui/DataGridWidget.hxx +++ b/src/debugger/gui/DataGridWidget.hxx @@ -102,6 +102,7 @@ class DataGridWidget : public EditableWidget void lostFocusWidget() override; bool hasToolTip() const override { return true; } + int getToolTipIndex(const Common::Point& pos) const; void handleMouseDown(int x, int y, MouseButton b, int clickCount) override; void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; @@ -150,7 +151,6 @@ class DataGridWidget : public EditableWidget void enableEditMode(bool state) { _editMode = state; } - int getToolTipIndex(const Common::Point& pos) const; private: // Following constructors and assignment operators not supported diff --git a/src/debugger/gui/RamWidget.cxx b/src/debugger/gui/RamWidget.cxx index 77456f810..7f651910a 100644 --- a/src/debugger/gui/RamWidget.cxx +++ b/src/debugger/gui/RamWidget.cxx @@ -15,7 +15,7 @@ // this file, and for a DISCLAIMER OF ALL WARRANTIES. //============================================================================ -#include "DataGridWidget.hxx" +#include "DataGridRamWidget.hxx" #include "EditTextWidget.hxx" #include "GuiObject.hxx" #include "InputTextDialog.hxx" @@ -54,8 +54,8 @@ RamWidget::RamWidget(GuiObject* boss, const GUI::Font& lfont, const GUI::Font& n // Add RAM grid (with scrollbar) int xpos = x + _font.getStringWidth("xxxx"); bool useScrollbar = ramsize / numrows > 16; - myRamGrid = new DataGridWidget(_boss, _nfont, xpos, ypos, - 16, myNumRows, 2, 8, Common::Base::Fmt::_16, useScrollbar); + myRamGrid = new DataGridRamWidget(_boss, *this, _nfont, xpos, ypos, + 16, myNumRows, 2, 8, Common::Base::Fmt::_16, useScrollbar); myRamGrid->setTarget(this); myRamGrid->setID(kRamGridID); addFocusWidget(myRamGrid); diff --git a/src/debugger/gui/RamWidget.hxx b/src/debugger/gui/RamWidget.hxx index fbc7747e6..069086fef 100644 --- a/src/debugger/gui/RamWidget.hxx +++ b/src/debugger/gui/RamWidget.hxx @@ -22,6 +22,7 @@ class GuiObject; class ButtonWidget; class DataGridWidget; class DataGridOpsWidget; +class DataGridRamWidget; class EditTextWidget; class StaticTextWidget; class InputTextDialog; @@ -41,11 +42,12 @@ class RamWidget : public Widget, public CommandSender void setOpsWidget(DataGridOpsWidget* w); void handleCommand(CommandSender* sender, int cmd, int data, int id) override; + virtual string getLabel(int addr) const = 0; + private: // To be implemented by derived classes virtual uInt8 getValue(int addr) const = 0; virtual void setValue(int addr, uInt8 value) = 0; - virtual string getLabel(int addr) const = 0; virtual void fillList(uInt32 start, uInt32 size, IntArray& alist, IntArray& vlist, @@ -97,7 +99,7 @@ class RamWidget : public Widget, public CommandSender StaticTextWidget* myRamStart{nullptr}; std::array myRamLabels{nullptr}; - DataGridWidget* myRamGrid{nullptr}; + DataGridRamWidget* myRamGrid{nullptr}; DataGridWidget* myHexValue{nullptr}; DataGridWidget* myDecValue{nullptr}; DataGridWidget* myBinValue{nullptr}; diff --git a/src/debugger/gui/RiotRamWidget.hxx b/src/debugger/gui/RiotRamWidget.hxx index 53d006bb0..3e30fc3e0 100644 --- a/src/debugger/gui/RiotRamWidget.hxx +++ b/src/debugger/gui/RiotRamWidget.hxx @@ -36,10 +36,11 @@ class RiotRamWidget : public RamWidget int x, int y, int w); ~RiotRamWidget() override = default; + string getLabel(int addr) const override; + private: uInt8 getValue(int addr) const override; void setValue(int addr, uInt8 value) override; - string getLabel(int addr) const override; void fillList(uInt32 start, uInt32 size, IntArray& alist, IntArray& vlist, BoolArray& changed) const override; diff --git a/src/debugger/gui/RiotWidget.cxx b/src/debugger/gui/RiotWidget.cxx index 73a70391f..053ff1325 100644 --- a/src/debugger/gui/RiotWidget.cxx +++ b/src/debugger/gui/RiotWidget.cxx @@ -66,11 +66,13 @@ RiotWidget::RiotWidget(GuiObject* boss, const GUI::Font& lfont, on.push_back("1"); } + StringList labels; + #define CREATE_IO_REGS(desc, bits, bitsID, editable) \ t = new StaticTextWidget(boss, lfont, xpos, ypos+2, lwidth, fontHeight,\ - desc, TextAlign::Left); \ + desc); \ xpos += t->getWidth() + 5; \ - bits = new ToggleBitWidget(boss, nfont, xpos, ypos, 8, 1); \ + bits = new ToggleBitWidget(boss, nfont, xpos, ypos, 8, 1, 1, labels); \ bits->setTarget(this); \ bits->setID(bitsID); \ if(editable) addFocusWidget(bits); else bits->setEditable(false); \ @@ -78,6 +80,7 @@ RiotWidget::RiotWidget(GuiObject* boss, const GUI::Font& lfont, bits->setList(off, on); // SWCHA bits in 'poke' mode + labels.clear(); CREATE_IO_REGS("SWCHA(W)", mySWCHAWriteBits, kSWCHABitsID, true) col = xpos + 20; // remember this for adding widgets to the second column @@ -87,10 +90,20 @@ RiotWidget::RiotWidget(GuiObject* boss, const GUI::Font& lfont, // SWCHA bits in 'peek' mode xpos = 10; ypos += lineHeight + 5; + labels.clear(); + labels.push_back("P0 right"); + labels.push_back("P0 left"); + labels.push_back("P0 down"); + labels.push_back("P0 up"); + labels.push_back("P1 right"); + labels.push_back("P1 left"); + labels.push_back("P1 down"); + labels.push_back("P1 up"); CREATE_IO_REGS("SWCHA(R)", mySWCHAReadBits, kSWCHARBitsID, true) // SWCHB bits in 'poke' mode xpos = 10; ypos += 2 * lineHeight; + labels.clear(); CREATE_IO_REGS("SWCHB(W)", mySWCHBWriteBits, kSWCHBBitsID, true) // SWBCNT bits @@ -99,6 +112,15 @@ RiotWidget::RiotWidget(GuiObject* boss, const GUI::Font& lfont, // SWCHB bits in 'peek' mode xpos = 10; ypos += lineHeight + 5; + labels.clear(); + labels.push_back("P1 difficulty"); + labels.push_back("P0 difficulty"); + labels.push_back(""); + labels.push_back(""); + labels.push_back("Color/B+W"); + labels.push_back(""); + labels.push_back("Select"); + labels.push_back("Reset"); CREATE_IO_REGS("SWCHB(R)", mySWCHBReadBits, kSWCHBRBitsID, true) // Timer registers (R/W) diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index 868b72cdf..aab298fad 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -452,7 +452,8 @@ Common::Point RomListWidget::getToolTipIndex(const Common::Point& pos) const const int col = (pos.x - r.x() - getAbsX()) / _font.getMaxCharWidth(); const int row = (pos.y - getAbsY()) / _lineHeight; - if(col < 0) + if(col < 0 || col >= 8 + || row < 0 || row + _currentPos >= int(myDisasm->list.size())) return Common::Point(-1, -1); else return Common::Point(col, row + _currentPos); @@ -463,7 +464,7 @@ string RomListWidget::getToolTip(const Common::Point& pos) const { const Common::Point& idx = getToolTipIndex(pos); - if(idx.y == -1) + if(idx.y < 0) return EmptyString; const string bytes = myDisasm->list[idx.y].bytes; @@ -480,12 +481,18 @@ string RomListWidget::getToolTip(const Common::Point& pos) const else { // 1..3 hex values - if(idx.x % 3 == 2) - // Skip gaps between hex values + if(idx.x == 2) + // Skip gap after first byte return EmptyString; - // Get one hex byte - const string valStr = bytes.substr((idx.x / 3) * 3, 2); + string valStr; + + if(idx.x < 2 || bytes.length() < 8) + // 1 or 2 hex bytes, get one hex byte + valStr = bytes.substr((idx.x / 3) * 3, 2); + else + // 3 hex bytes, get two rightmost hex bytes + valStr = bytes.substr(6, 2) + bytes.substr(3, 2); val = static_cast(stol(valStr, nullptr, 16)); } @@ -495,7 +502,11 @@ string RomListWidget::getToolTip(const Common::Point& pos) const << "$" << Common::Base::toString(val, Common::Base::Fmt::_16) << " = #" << val; if(val < 0x100) + { + if(val >= 0x80) + buf << '/' << -(0x100 - val); buf << " = %" << Common::Base::toString(val, Common::Base::Fmt::_2); + } return buf.str(); } diff --git a/src/debugger/gui/ToggleBitWidget.cxx b/src/debugger/gui/ToggleBitWidget.cxx index 39ca7dcce..212ae60ed 100644 --- a/src/debugger/gui/ToggleBitWidget.cxx +++ b/src/debugger/gui/ToggleBitWidget.cxx @@ -25,8 +25,10 @@ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ToggleBitWidget::ToggleBitWidget(GuiObject* boss, const GUI::Font& font, - int x, int y, int cols, int rows, int colchars) - : ToggleWidget(boss, font, x, y, cols, rows) + int x, int y, int cols, int rows, int colchars, + const StringList& labels) + : ToggleWidget(boss, font, x, y, cols, rows), + _labelList(labels) { _rowHeight = font.getLineHeight(); _colWidth = colchars * font.getMaxCharWidth() + 8; @@ -47,6 +49,13 @@ ToggleBitWidget::ToggleBitWidget(GuiObject* boss, const GUI::Font& font, _h = _rowHeight * rows + 1; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +ToggleBitWidget::ToggleBitWidget(GuiObject* boss, const GUI::Font& font, + int x, int y, int cols, int rows, int colchars) + : ToggleBitWidget(boss, font, x, y, cols, rows, colchars, StringList()) +{ +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToggleBitWidget::setList(const StringList& off, const StringList& on) { @@ -69,6 +78,27 @@ void ToggleBitWidget::setState(const BoolArray& state, const BoolArray& changed) setDirty(); } + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string ToggleBitWidget::getToolTip(const Common::Point& pos) const +{ + Common::Point idx = getToolTipIndex(pos); + + if(idx.y < 0) + return EmptyString; + + const string tip = ToggleWidget::getToolTip(pos); + + if(idx.x < _labelList.size()) + { + const string label = _labelList[idx.x]; + + if(!label.empty()) + return tip + "\n" + label; + } + return tip; +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToggleBitWidget::drawWidget(bool hilite) { diff --git a/src/debugger/gui/ToggleBitWidget.hxx b/src/debugger/gui/ToggleBitWidget.hxx index 6d5505030..707aea1cc 100644 --- a/src/debugger/gui/ToggleBitWidget.hxx +++ b/src/debugger/gui/ToggleBitWidget.hxx @@ -26,17 +26,23 @@ class ToggleBitWidget : public ToggleWidget public: ToggleBitWidget(GuiObject* boss, const GUI::Font& font, int x, int y, int cols, int rows, int colchars = 1); + ToggleBitWidget(GuiObject* boss, const GUI::Font& font, + int x, int y, int cols, int rows, int colchars, + const StringList& labels); ~ToggleBitWidget() override = default; void setList(const StringList& off, const StringList& on); void setState(const BoolArray& state, const BoolArray& changed); + string getToolTip(const Common::Point& pos) const override; + protected: void drawWidget(bool hilite) override; protected: StringList _offList; StringList _onList; + StringList _labelList; private: // Following constructors and assignment operators not supported diff --git a/src/debugger/gui/ToggleWidget.cxx b/src/debugger/gui/ToggleWidget.cxx index ac321f4bf..adb33e118 100644 --- a/src/debugger/gui/ToggleWidget.cxx +++ b/src/debugger/gui/ToggleWidget.cxx @@ -209,17 +209,25 @@ void ToggleWidget::handleCommand(CommandSender* sender, int cmd, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -int ToggleWidget::getToolTipIndex(const Common::Point& pos) const +Common::Point ToggleWidget::getToolTipIndex(const Common::Point& pos) const { + const int col = (pos.x - getAbsX()) / _colWidth; const int row = (pos.y - getAbsY()) / _rowHeight; - return row * _cols; + if(row >= 0 && row < _rows && col >= 0 && col < _cols) + return Common::Point(col, row); + else + return Common::Point(-1, -1); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - string ToggleWidget::getToolTip(const Common::Point& pos) const { - const int idx = getToolTipIndex(pos); + const int idx = getToolTipIndex(pos).y * _cols; + + if(idx < 0) + return EmptyString; + Int32 val = 0; ostringstream buf; diff --git a/src/debugger/gui/ToggleWidget.hxx b/src/debugger/gui/ToggleWidget.hxx index 347359677..ee70f6233 100644 --- a/src/debugger/gui/ToggleWidget.hxx +++ b/src/debugger/gui/ToggleWidget.hxx @@ -51,6 +51,7 @@ class ToggleWidget : public Widget, public CommandSender protected: bool hasToolTip() const override { return true; } + Common::Point getToolTipIndex(const Common::Point& pos) const; protected: int _rows{0}; @@ -76,8 +77,6 @@ class ToggleWidget : public Widget, public CommandSender bool handleKeyDown(StellaKey key, StellaMod mod) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override; - int getToolTipIndex(const Common::Point& pos) const; - // Following constructors and assignment operators not supported ToggleWidget() = delete; ToggleWidget(const ToggleWidget&) = delete; diff --git a/src/debugger/gui/module.mk b/src/debugger/gui/module.mk index cda7666e9..0a985e71b 100644 --- a/src/debugger/gui/module.mk +++ b/src/debugger/gui/module.mk @@ -55,6 +55,7 @@ MODULE_OBJS := \ src/debugger/gui/CartDebugWidget.o \ src/debugger/gui/CpuWidget.o \ src/debugger/gui/DataGridOpsWidget.o \ + src/debugger/gui/DataGridRamWidget.o \ src/debugger/gui/DataGridWidget.o \ src/debugger/gui/DebuggerDialog.o \ src/debugger/gui/DelayQueueWidget.o \ diff --git a/src/windows/Stella.vcxproj b/src/windows/Stella.vcxproj index f9b5d3499..76e311ce3 100644 --- a/src/windows/Stella.vcxproj +++ b/src/windows/Stella.vcxproj @@ -525,6 +525,9 @@ true + + true + true @@ -1550,6 +1553,9 @@ true + + true + true diff --git a/src/windows/Stella.vcxproj.filters b/src/windows/Stella.vcxproj.filters index 550419e9a..6db5675f0 100644 --- a/src/windows/Stella.vcxproj.filters +++ b/src/windows/Stella.vcxproj.filters @@ -1035,6 +1035,9 @@ Source Files\gui + + Source Files\debugger + @@ -2129,6 +2132,9 @@ Header Files\gui + + Header Files\debugger + From b77afae17840dae057ad14d70e9e58181b81e57c Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 19 Nov 2020 12:26:03 +0100 Subject: [PATCH 067/107] oops, added missing files --- src/debugger/DataGridRamWidget.cxx | 54 ++++++++++++++++++++++++++++++ src/debugger/DataGridRamWidget.hxx | 51 ++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/debugger/DataGridRamWidget.cxx create mode 100644 src/debugger/DataGridRamWidget.hxx diff --git a/src/debugger/DataGridRamWidget.cxx b/src/debugger/DataGridRamWidget.cxx new file mode 100644 index 000000000..9be4c4f14 --- /dev/null +++ b/src/debugger/DataGridRamWidget.cxx @@ -0,0 +1,54 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2020 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "RamWidget.hxx" +#include "DataGridRamWidget.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +DataGridRamWidget::DataGridRamWidget(GuiObject* boss, const RamWidget& ram, + const GUI::Font& font, + int x, int y, int cols, int rows, + int colchars, int bits, + Common::Base::Fmt base, + bool useScrollbar) + : DataGridWidget(boss, font, x, y, cols, rows, colchars, + bits, base, useScrollbar), + _ram(ram) +{ +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string DataGridRamWidget::getToolTip(const Common::Point& pos) const +{ + const int idx = getToolTipIndex(pos); + + if(idx < 0) + return EmptyString; + + const Int32 addr = _addrList[idx]; + const string label = _ram.getLabel(addr); + const string tip = DataGridWidget::getToolTip(pos); + + if(label.empty()) + return tip; + + ostringstream buf; + + buf << _ram.getLabel(addr) << '\n' << tip; + + return buf.str(); +} diff --git a/src/debugger/DataGridRamWidget.hxx b/src/debugger/DataGridRamWidget.hxx new file mode 100644 index 000000000..451c23403 --- /dev/null +++ b/src/debugger/DataGridRamWidget.hxx @@ -0,0 +1,51 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2020 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef DATA_GRID_RAM_WIDGET_HXX +#define DATA_GRID_RAM_WIDGET_HXX + +class RamWidget; + +#include "DataGridWidget.hxx" +#include "Base.hxx" + +class DataGridRamWidget : public DataGridWidget +{ + public: + DataGridRamWidget(GuiObject* boss, const RamWidget& ram, + const GUI::Font& font, + int x, int y, int cols, int rows, + int colchars, int bits, + Common::Base::Fmt format = Common::Base::Fmt::_DEFAULT, + bool useScrollbar = false); + ~DataGridRamWidget() override = default; + + string getToolTip(const Common::Point& pos) const override; + + private: + const RamWidget& _ram; + + private: + // Following constructors and assignment operators not supported + DataGridRamWidget() = delete; + DataGridRamWidget(const DataGridRamWidget&) = delete; + DataGridRamWidget(DataGridRamWidget&&) = delete; + DataGridRamWidget& operator=(const DataGridRamWidget&) = delete; + DataGridRamWidget& operator=(DataGridRamWidget&&) = delete; +}; + +#endif From 4f43334b6ce663d2a4bb3f1ce196c655ac7a1464 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Thu, 19 Nov 2020 09:36:02 -0330 Subject: [PATCH 068/107] Fix wrong location of debugger files. --- src/debugger/{ => gui}/DataGridRamWidget.cxx | 0 src/debugger/{ => gui}/DataGridRamWidget.hxx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/debugger/{ => gui}/DataGridRamWidget.cxx (100%) rename src/debugger/{ => gui}/DataGridRamWidget.hxx (100%) diff --git a/src/debugger/DataGridRamWidget.cxx b/src/debugger/gui/DataGridRamWidget.cxx similarity index 100% rename from src/debugger/DataGridRamWidget.cxx rename to src/debugger/gui/DataGridRamWidget.cxx diff --git a/src/debugger/DataGridRamWidget.hxx b/src/debugger/gui/DataGridRamWidget.hxx similarity index 100% rename from src/debugger/DataGridRamWidget.hxx rename to src/debugger/gui/DataGridRamWidget.hxx From 2abfd14d46e3271edf2aa775e90e9b42f0d5217f Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Thu, 19 Nov 2020 09:49:38 -0330 Subject: [PATCH 069/107] Fix location of files in VS project. --- src/windows/Stella.vcxproj | 8 ++------ src/windows/Stella.vcxproj.filters | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/windows/Stella.vcxproj b/src/windows/Stella.vcxproj index 76e311ce3..d1cf96c6b 100644 --- a/src/windows/Stella.vcxproj +++ b/src/windows/Stella.vcxproj @@ -525,9 +525,6 @@ true - - true - true @@ -684,6 +681,7 @@ true + true @@ -1553,9 +1551,6 @@ true - - true - true @@ -1712,6 +1707,7 @@ true + true diff --git a/src/windows/Stella.vcxproj.filters b/src/windows/Stella.vcxproj.filters index 6db5675f0..a8cac63d6 100644 --- a/src/windows/Stella.vcxproj.filters +++ b/src/windows/Stella.vcxproj.filters @@ -1035,7 +1035,7 @@ Source Files\gui - + Source Files\debugger @@ -2132,7 +2132,7 @@ Header Files\gui - + Header Files\debugger From 7112dc553370982120ae1f376fe0975d83d5558b Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Thu, 19 Nov 2020 10:29:41 -0330 Subject: [PATCH 070/107] Add debugger files to Xcode project, and fix minor warnings. --- src/debugger/gui/ToggleBitWidget.cxx | 4 ++-- src/macos/stella.xcodeproj/project.pbxproj | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/debugger/gui/ToggleBitWidget.cxx b/src/debugger/gui/ToggleBitWidget.cxx index 212ae60ed..1ebe1c5e4 100644 --- a/src/debugger/gui/ToggleBitWidget.cxx +++ b/src/debugger/gui/ToggleBitWidget.cxx @@ -82,14 +82,14 @@ void ToggleBitWidget::setState(const BoolArray& state, const BoolArray& changed) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - string ToggleBitWidget::getToolTip(const Common::Point& pos) const { - Common::Point idx = getToolTipIndex(pos); + const Common::Point& idx = getToolTipIndex(pos); if(idx.y < 0) return EmptyString; const string tip = ToggleWidget::getToolTip(pos); - if(idx.x < _labelList.size()) + if(idx.x < static_cast(_labelList.size())) { const string label = _labelList[idx.x]; diff --git a/src/macos/stella.xcodeproj/project.pbxproj b/src/macos/stella.xcodeproj/project.pbxproj index a23a0b173..09a728c1b 100644 --- a/src/macos/stella.xcodeproj/project.pbxproj +++ b/src/macos/stella.xcodeproj/project.pbxproj @@ -533,6 +533,8 @@ DCBDDE9F1D6A5F2F009DF1E9 /* Cart3EPlus.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCBDDE9D1D6A5F2F009DF1E9 /* Cart3EPlus.hxx */; }; DCC2FDF5255EB82500FA5E81 /* ToolTip.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC2FDF3255EB82500FA5E81 /* ToolTip.hxx */; }; DCC2FDF6255EB82500FA5E81 /* ToolTip.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCC2FDF4255EB82500FA5E81 /* ToolTip.cxx */; }; + DCC2FDF92566AD8800FA5E81 /* DataGridRamWidget.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCC2FDF72566AD8800FA5E81 /* DataGridRamWidget.cxx */; }; + DCC2FDFA2566AD8800FA5E81 /* DataGridRamWidget.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC2FDF82566AD8800FA5E81 /* DataGridRamWidget.hxx */; }; DCC527D110B9DA19005E1287 /* Device.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC527C910B9DA19005E1287 /* Device.hxx */; }; DCC527D210B9DA19005E1287 /* M6502.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCC527CA10B9DA19005E1287 /* M6502.cxx */; }; DCC527D310B9DA19005E1287 /* M6502.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC527CB10B9DA19005E1287 /* M6502.hxx */; }; @@ -1302,6 +1304,8 @@ DCBDDE9D1D6A5F2F009DF1E9 /* Cart3EPlus.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Cart3EPlus.hxx; sourceTree = ""; }; DCC2FDF3255EB82500FA5E81 /* ToolTip.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ToolTip.hxx; sourceTree = ""; }; DCC2FDF4255EB82500FA5E81 /* ToolTip.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ToolTip.cxx; sourceTree = ""; }; + DCC2FDF72566AD8800FA5E81 /* DataGridRamWidget.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = DataGridRamWidget.cxx; sourceTree = ""; }; + DCC2FDF82566AD8800FA5E81 /* DataGridRamWidget.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = DataGridRamWidget.hxx; sourceTree = ""; }; DCC527C910B9DA19005E1287 /* Device.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Device.hxx; sourceTree = ""; }; DCC527CA10B9DA19005E1287 /* M6502.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = M6502.cxx; sourceTree = ""; }; DCC527CB10B9DA19005E1287 /* M6502.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = M6502.hxx; sourceTree = ""; }; @@ -1706,6 +1710,8 @@ 2D20F9E708C603EC00A73076 /* CpuWidget.hxx */, 2D20F9E808C603EC00A73076 /* DataGridOpsWidget.cxx */, 2D20F9E908C603EC00A73076 /* DataGridOpsWidget.hxx */, + DCC2FDF72566AD8800FA5E81 /* DataGridRamWidget.cxx */, + DCC2FDF82566AD8800FA5E81 /* DataGridRamWidget.hxx */, 2D20F9EA08C603EC00A73076 /* DataGridWidget.cxx */, 2D20F9EB08C603EC00A73076 /* DataGridWidget.hxx */, 2D20F9EC08C603EC00A73076 /* DebuggerDialog.cxx */, @@ -2578,6 +2584,7 @@ E08FCD5823A037EB0051F59B /* QisBlitter.hxx in Headers */, DCA078351F8C1B04008EFEE5 /* SDL_lib.hxx in Headers */, DCDA03B11A2009BB00711920 /* CartWD.hxx in Headers */, + DCC2FDFA2566AD8800FA5E81 /* DataGridRamWidget.hxx in Headers */, 2D91745909BA90380026E9FF /* PromptWidget.hxx in Headers */, DC3C9BC62469C8F700CF2D47 /* PaletteHandler.hxx in Headers */, 2D91745A09BA90380026E9FF /* RamWidget.hxx in Headers */, @@ -3198,6 +3205,7 @@ DC676A591729A0B000E4E73D /* CartSBWidget.cxx in Sources */, DC3C9BCC2469C93D00CF2D47 /* EmulationDialog.cxx in Sources */, DC676A5B1729A0B000E4E73D /* CartX07Widget.cxx in Sources */, + DCC2FDF92566AD8800FA5E81 /* DataGridRamWidget.cxx in Sources */, DC7A24DF173B1DBC00B20FE9 /* FileListWidget.cxx in Sources */, DC13B53F176FF2F500B8B4BB /* RomListSettings.cxx in Sources */, DC3EE8581E2C0E6D00905161 /* crc32.c in Sources */, From 319c521b77fedc204d9e90f0065714b1195d79d5 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Thu, 19 Nov 2020 11:35:02 -0330 Subject: [PATCH 071/107] Enable RTTI by default for Linux/UNIX builds; disable it for release builds only. --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 90279a56a..87a2c9847 100644 --- a/Makefile +++ b/Makefile @@ -48,11 +48,11 @@ endif CXXFLAGS+= -Wall -Wextra -Wno-unused-parameter ifdef HAVE_GCC - CXXFLAGS+= -Wno-multichar -Wunused -fno-rtti -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 -frtti + CXXFLAGS+= -Wno-multichar -Wunused -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 endif ifdef HAVE_CLANG - CXXFLAGS+= -Wno-multichar -Wunused -frtti -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 + CXXFLAGS+= -Wno-multichar -Wunused -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 endif ifdef CLANG_WARNINGS @@ -81,8 +81,8 @@ else endif ifdef RELEASE - CXXFLAGS += -flto - LDFLAGS += -flto + CXXFLAGS += -flto -fno-rtti + LDFLAGS += -flto -fno-rtti endif ####################################################################### From 3c50de30a6d32724f8a02aa858006595e4a9ec77 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 19 Nov 2020 16:40:16 +0100 Subject: [PATCH 072/107] added tooltip hiding when context menus are opened added tooltips to TiaOutputWidget and TiaZoomWidget --- src/debugger/gui/RomListWidget.cxx | 2 +- src/debugger/gui/TiaOutputWidget.cxx | 49 ++++++++++++++++++++++++++++ src/debugger/gui/TiaOutputWidget.hxx | 7 ++++ src/debugger/gui/TiaZoomWidget.cxx | 46 ++++++++++++++++++++++++++ src/debugger/gui/TiaZoomWidget.hxx | 7 ++++ src/gui/LauncherDialog.cxx | 1 + 6 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index aab298fad..d732cd328 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -247,6 +247,7 @@ void RomListWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) // Set selected and add menu at current x,y mouse location _selectedItem = findItem(x, y); scrollToSelected(); + dialog().tooltip().hide(); myMenu->show(x + getAbsX(), y + getAbsY(), dialog().surface().dstRect(), _selectedItem); } @@ -518,7 +519,6 @@ bool RomListWidget::changedToolTip(const Common::Point& oldPos, return getToolTipIndex(oldPos) != getToolTipIndex(newPos); } - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void RomListWidget::drawWidget(bool hilite) { diff --git a/src/debugger/gui/TiaOutputWidget.cxx b/src/debugger/gui/TiaOutputWidget.cxx index 2062bd436..94963f870 100644 --- a/src/debugger/gui/TiaOutputWidget.cxx +++ b/src/debugger/gui/TiaOutputWidget.cxx @@ -22,6 +22,8 @@ #include "FBSurface.hxx" #include "Widget.hxx" #include "GuiObject.hxx" +#include "Dialog.hxx" +#include "ToolTip.hxx" #include "ContextMenu.hxx" #include "TiaZoomWidget.hxx" #include "Debugger.hxx" @@ -55,6 +57,7 @@ TiaOutputWidget::TiaOutputWidget(GuiObject* boss, const GUI::Font& font, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TiaOutputWidget::loadConfig() { + setEnabled(true); setDirty(); } @@ -110,6 +113,7 @@ void TiaOutputWidget::handleMouseDown(int x, int y, MouseButton b, int clickCoun myClickX = x; myClickY = y; + dialog().tooltip().hide(); // Add menu at current x,y mouse location myMenu->show(x + getAbsX(), y + getAbsY(), dialog().surface().dstRect()); } @@ -158,6 +162,51 @@ void TiaOutputWidget::handleCommand(CommandSender* sender, int cmd, int data, in } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Common::Point TiaOutputWidget::getToolTipIndex(const Common::Point& pos) const +{ + const Int32 width = instance().console().tia().width(); + const Int32 height = instance().console().tia().height(); + const int col = (pos.x - 1 - getAbsX()) >> 1; + const int row = pos.y - 1 - getAbsY(); + + if(col < 0 || col >= width || row < 0 || row >= height) + return Common::Point(-1, -1); + else + return Common::Point(col, row); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string TiaOutputWidget::getToolTip(const Common::Point& pos) const +{ + Common::Point idx = getToolTipIndex(pos); + + if(idx.x < 0) + return EmptyString; + + uInt32 height = instance().console().tia().height(); + // limit to 274 lines (PAL default without scaling) + uInt32 yStart = height <= FrameManager::Metrics::baseHeightPAL + ? 0 : (height - FrameManager::Metrics::baseHeightPAL) >> 1; + const Int32 i = idx.x + (yStart + idx.y) * instance().console().tia().width(); + uInt8* tiaOutputBuffer = instance().console().tia().outputBuffer(); + ostringstream buf; + + buf << _toolTipText + << "X: #" << idx.x + << "\nY: #" << idx.y + << "\nC: $" << Common::Base::toString(tiaOutputBuffer[i], Common::Base::Fmt::_16); + + return buf.str(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool TiaOutputWidget::changedToolTip(const Common::Point& oldPos, + const Common::Point& newPos) const +{ + return getToolTipIndex(oldPos) != getToolTipIndex(newPos); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TiaOutputWidget::drawWidget(bool hilite) { diff --git a/src/debugger/gui/TiaOutputWidget.hxx b/src/debugger/gui/TiaOutputWidget.hxx index c7bfd26f6..cc465266a 100644 --- a/src/debugger/gui/TiaOutputWidget.hxx +++ b/src/debugger/gui/TiaOutputWidget.hxx @@ -47,6 +47,13 @@ class TiaOutputWidget : public Widget, public CommandSender bool handleKeyDown(StellaKey key, StellaMod mod) override; bool handleKeyUp(StellaKey key, StellaMod mod) override; */ + string getToolTip(const Common::Point& pos) const override; + bool changedToolTip(const Common::Point& oldPos, const Common::Point& newPos) const override; + + protected: + bool hasToolTip() const override { return true; } + Common::Point getToolTipIndex(const Common::Point& pos) const; + private: unique_ptr myMenu; TiaZoomWidget* myZoom{nullptr}; diff --git a/src/debugger/gui/TiaZoomWidget.cxx b/src/debugger/gui/TiaZoomWidget.cxx index 0895c51a8..eec9723ff 100644 --- a/src/debugger/gui/TiaZoomWidget.cxx +++ b/src/debugger/gui/TiaZoomWidget.cxx @@ -26,6 +26,8 @@ #include "FBSurface.hxx" #include "Widget.hxx" #include "GuiObject.hxx" +#include "Dialog.hxx" +#include "ToolTip.hxx" #include "ContextMenu.hxx" #include "FrameManager.hxx" #include "TiaZoomWidget.hxx" @@ -127,6 +129,7 @@ void TiaZoomWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) } else if(b == MouseButton::RIGHT) { + dialog().tooltip().hide(); // Add menu at current x,y mouse location myMenu->show(x + getAbsX(), y + getAbsY(), dialog().surface().dstRect()); } @@ -141,6 +144,8 @@ void TiaZoomWidget::handleMouseUp(int x, int y, MouseButton b, int clickCount) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TiaZoomWidget::handleMouseWheel(int x, int y, int direction) { + dialog().tooltip().hide(); + // zoom towards mouse position myClickX = x; myClickY = y; @@ -274,6 +279,47 @@ void TiaZoomWidget::handleCommand(CommandSender* sender, int cmd, int data, int } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Common::Point TiaZoomWidget::getToolTipIndex(const Common::Point& pos) const +{ + const Int32 width = instance().console().tia().width() * 2; + const Int32 height = instance().console().tia().height(); + const int col = (pos.x - 1 - getAbsX()) / (myZoomLevel << 1) + (myOffX >> 1); + const int row = (pos.y - 1 - getAbsY()) / myZoomLevel + myOffY; + + if(col < 0 || col >= width || row < 0 || row >= height) + return Common::Point(-1, -1); + else + return Common::Point(col, row); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string TiaZoomWidget::getToolTip(const Common::Point& pos) const +{ + Common::Point idx = getToolTipIndex(pos); + + if(idx.x < 0) + return EmptyString; + + const Int32 i = idx.x + idx.y * instance().console().tia().width(); + uInt8* tiaOutputBuffer = instance().console().tia().outputBuffer(); + ostringstream buf; + + buf << _toolTipText + << "X: #" << idx.x + << "\nY: #" << idx.y + << "\nC: $" << Common::Base::toString(tiaOutputBuffer[i], Common::Base::Fmt::_16); + + return buf.str(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool TiaZoomWidget::changedToolTip(const Common::Point& oldPos, + const Common::Point& newPos) const +{ + return getToolTipIndex(oldPos) != getToolTipIndex(newPos); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TiaZoomWidget::drawWidget(bool hilite) { diff --git a/src/debugger/gui/TiaZoomWidget.hxx b/src/debugger/gui/TiaZoomWidget.hxx index 7169645bf..d46787f23 100644 --- a/src/debugger/gui/TiaZoomWidget.hxx +++ b/src/debugger/gui/TiaZoomWidget.hxx @@ -34,6 +34,13 @@ class TiaZoomWidget : public Widget, public CommandSender void loadConfig() override; void setPos(int x, int y); + string getToolTip(const Common::Point& pos) const override; + bool changedToolTip(const Common::Point& oldPos, const Common::Point& newPos) const override; + + protected: + bool hasToolTip() const override { return true; } + Common::Point getToolTipIndex(const Common::Point& pos) const; + private: void zoom(int level); void recalc(); diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 5092299ec..405be5745 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -568,6 +568,7 @@ void LauncherDialog::handleMouseDown(int x, int y, MouseButton b, int clickCount // Grab right mouse button for context menu, send left to base class if(b == MouseButton::RIGHT) { + dialog().tooltip().hide(); // Dynamically create context menu for ROM list options VariantList items; From 779375abec6c4d8a2df48dc03225b49c7c379ad8 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 19 Nov 2020 16:44:03 +0100 Subject: [PATCH 073/107] fixed potential Clang warnings --- src/debugger/gui/TiaOutputWidget.cxx | 6 +++--- src/debugger/gui/TiaZoomWidget.cxx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/debugger/gui/TiaOutputWidget.cxx b/src/debugger/gui/TiaOutputWidget.cxx index 94963f870..6d2a1b9cb 100644 --- a/src/debugger/gui/TiaOutputWidget.cxx +++ b/src/debugger/gui/TiaOutputWidget.cxx @@ -179,14 +179,14 @@ Common::Point TiaOutputWidget::getToolTipIndex(const Common::Point& pos) const // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - string TiaOutputWidget::getToolTip(const Common::Point& pos) const { - Common::Point idx = getToolTipIndex(pos); + const Common::Point& idx = getToolTipIndex(pos); if(idx.x < 0) return EmptyString; - uInt32 height = instance().console().tia().height(); + const uInt32 height = instance().console().tia().height(); // limit to 274 lines (PAL default without scaling) - uInt32 yStart = height <= FrameManager::Metrics::baseHeightPAL + const uInt32 yStart = height <= FrameManager::Metrics::baseHeightPAL ? 0 : (height - FrameManager::Metrics::baseHeightPAL) >> 1; const Int32 i = idx.x + (yStart + idx.y) * instance().console().tia().width(); uInt8* tiaOutputBuffer = instance().console().tia().outputBuffer(); diff --git a/src/debugger/gui/TiaZoomWidget.cxx b/src/debugger/gui/TiaZoomWidget.cxx index eec9723ff..3cbbad8e2 100644 --- a/src/debugger/gui/TiaZoomWidget.cxx +++ b/src/debugger/gui/TiaZoomWidget.cxx @@ -296,7 +296,7 @@ Common::Point TiaZoomWidget::getToolTipIndex(const Common::Point& pos) const // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - string TiaZoomWidget::getToolTip(const Common::Point& pos) const { - Common::Point idx = getToolTipIndex(pos); + const Common::Point& idx = getToolTipIndex(pos); if(idx.x < 0) return EmptyString; From 4d19473bb4c204dbc7bab454321a2f6c9606a5ab Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Thu, 19 Nov 2020 13:39:32 -0330 Subject: [PATCH 074/107] Enable RTTI for Xcode. --- src/macos/stella.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/macos/stella.xcodeproj/project.pbxproj b/src/macos/stella.xcodeproj/project.pbxproj index 09a728c1b..d0892bdce 100644 --- a/src/macos/stella.xcodeproj/project.pbxproj +++ b/src/macos/stella.xcodeproj/project.pbxproj @@ -3405,7 +3405,7 @@ ., "$(HOME)/Library/Frameworks", ); - GCC_ENABLE_CPP_RTTI = NO; + GCC_ENABLE_CPP_RTTI = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_VERSION = ""; @@ -3480,7 +3480,7 @@ ., "$(HOME)/Library/Frameworks", ); - GCC_ENABLE_CPP_RTTI = NO; + GCC_ENABLE_CPP_RTTI = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 3; GCC_VERSION = ""; From 80efc3d63107b09cc9a0b865feb515cf3ac53862 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 19 Nov 2020 19:19:49 +0100 Subject: [PATCH 075/107] removed "on top" logic for drawing widgets --- src/debugger/gui/DataGridWidget.cxx | 10 +++---- src/debugger/gui/DelayQueueWidget.cxx | 5 ++-- src/debugger/gui/PromptWidget.cxx | 8 ++--- src/debugger/gui/RomListWidget.cxx | 5 ++-- src/debugger/gui/ToggleBitWidget.cxx | 13 ++++---- src/debugger/gui/TogglePixelWidget.cxx | 3 +- src/gui/CheckListWidget.cxx | 8 ++--- src/gui/ColorWidget.cxx | 5 ++-- src/gui/Dialog.hxx | 1 - src/gui/EditTextWidget.cxx | 9 +++--- src/gui/PopUpWidget.cxx | 11 ++++--- src/gui/RomInfoWidget.cxx | 8 ++--- src/gui/ScrollBarWidget.cxx | 14 ++++----- src/gui/StringListWidget.cxx | 9 +++--- src/gui/TabWidget.cxx | 16 +++++----- src/gui/Widget.cxx | 41 ++++++++------------------ 16 files changed, 64 insertions(+), 102 deletions(-) diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index 5d0dc0623..81840147a 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -620,10 +620,9 @@ bool DataGridWidget::changedToolTip(const Common::Point& oldPos, void DataGridWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); int row, col; - s.fillRect(_x, _y, _w, _h, hilite && isEnabled() && isEditable() ? _bgcolorhi : onTop ? _bgcolor : kBGColorHi); + s.fillRect(_x, _y, _w, _h, hilite && isEnabled() && isEditable() ? _bgcolorhi : _bgcolor); // Draw the internal grid and labels int linewidth = _cols * _colWidth; s.frameRect(_x, _y, _w, _h, hilite && isEnabled() && isEditable() ? kWidColorHi : kColor); @@ -642,7 +641,7 @@ void DataGridWidget::drawWidget(bool hilite) int x = _x + 4 + (col * _colWidth); int y = _y + 2 + (row * _rowHeight); int pos = row*_cols + col; - ColorId textColor = onTop ? kTextColor : kColor; + ColorId textColor = kTextColor; // Draw the selected item inverted, on a highlighted background. if (_currentRow == row && _currentCol == col && @@ -662,13 +661,12 @@ void DataGridWidget::drawWidget(bool hilite) { if(_changedList[pos]) { - s.fillRect(x - 3, y - 1, _colWidth-1, _rowHeight-1, - onTop ? kDbgChangedColor : _bgcolorlo); + s.fillRect(x - 3, y - 1, _colWidth-1, _rowHeight-1, kDbgChangedColor); if(_hiliteList[pos]) textColor = kDbgColorHi; else - textColor = onTop ? kDbgChangedTextColor : textColor; + textColor = kDbgChangedTextColor; } else if(_hiliteList[pos]) textColor = kDbgColorHi; diff --git a/src/debugger/gui/DelayQueueWidget.cxx b/src/debugger/gui/DelayQueueWidget.cxx index 6dbd613be..cca5644c6 100644 --- a/src/debugger/gui/DelayQueueWidget.cxx +++ b/src/debugger/gui/DelayQueueWidget.cxx @@ -90,7 +90,6 @@ void DelayQueueWidget::loadConfig() { void DelayQueueWidget::drawWidget(bool hilite) { FBSurface& surface = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); int y = _y, x = _x, @@ -102,14 +101,14 @@ void DelayQueueWidget::drawWidget(bool hilite) y += 1; x += 1; w -= 1; - surface.fillRect(x, y, w - 1, _h - 2, onTop ? kDlgColor : _bgcolorlo); + surface.fillRect(x, y, w - 1, _h - 2, kDlgColor); y += 2; x += 2; w -= 3; for (const auto& line : myLines) { - surface.drawString(_font, line, x, y, w, onTop ? _textcolor : kColor); + surface.drawString(_font, line, x, y, w, _textcolor); y += lineHeight; } } diff --git a/src/debugger/gui/PromptWidget.cxx b/src/debugger/gui/PromptWidget.cxx index 490f7b8a6..116080bf7 100644 --- a/src/debugger/gui/PromptWidget.cxx +++ b/src/debugger/gui/PromptWidget.cxx @@ -81,9 +81,7 @@ void PromptWidget::drawWidget(bool hilite) { //cerr << "PromptWidget::drawWidget\n"; ColorId fgcolor, bgcolor; - FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); // Draw text int start = _scrollLine - _linesPerPage + 1; @@ -104,7 +102,7 @@ void PromptWidget::drawWidget(bool hilite) else fgcolor = ColorId(c >> 8); - s.drawChar(_font, c & 0x7f, x, y, onTop ? fgcolor : kColor); + s.drawChar(_font, c & 0x7f, x, y, fgcolor); x += _kConsoleCharWidth; } y += _kConsoleLineHeight; @@ -938,8 +936,6 @@ void PromptWidget::drawCaret() { //cerr << "PromptWidget::drawCaret()\n"; FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); - int line = _currentPos / _lineWidth; // Don't draw the cursor if it's not in the current view @@ -951,7 +947,7 @@ void PromptWidget::drawCaret() int y = _y + displayLine * _kConsoleLineHeight; char c = buffer(_currentPos); //FIXME: int to char?? - s.fillRect(x, y, _kConsoleCharWidth, _kConsoleLineHeight, onTop ? kTextColor : kColor); + s.fillRect(x, y, _kConsoleCharWidth, _kConsoleLineHeight, kTextColor); s.drawChar(_font, c, x, y + 2, kBGColor); } diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index d732cd328..c226398a6 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -523,10 +523,9 @@ bool RomListWidget::changedToolTip(const Common::Point& oldPos, void RomListWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); const CartDebug::DisassemblyList& dlist = myDisasm->list; int i, pos, xpos, ypos, len = int(dlist.size()); - ColorId textColor = onTop ? kTextColor : kColor; + ColorId textColor = kTextColor; const Common::Rect& r = getEditRect(); const Common::Rect& l = getLineRect(); @@ -559,7 +558,7 @@ void RomListWidget::drawWidget(bool hilite) // Draw highlighted item in a frame if(_highlightedItem == pos) - s.frameRect(_x + l.x() - 3, ypos - 1, _w - l.x(), _lineHeight, onTop ? kWidColorHi : kBGColorLo); + s.frameRect(_x + l.x() - 3, ypos - 1, _w - l.x(), _lineHeight, kWidColorHi); // Draw the selected item inverted, on a highlighted background. if(_selectedItem == pos && _hasFocus) diff --git a/src/debugger/gui/ToggleBitWidget.cxx b/src/debugger/gui/ToggleBitWidget.cxx index 1ebe1c5e4..994d73aa9 100644 --- a/src/debugger/gui/ToggleBitWidget.cxx +++ b/src/debugger/gui/ToggleBitWidget.cxx @@ -104,7 +104,6 @@ void ToggleBitWidget::drawWidget(bool hilite) { //cerr << "ToggleBitWidget::drawWidget\n"; FBSurface& s = dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); int row, col; string buffer; @@ -146,18 +145,16 @@ void ToggleBitWidget::drawWidget(bool hilite) // Highlight changes if(_changedList[pos]) { - s.fillRect(x - 3, y - 1, _colWidth-1, _rowHeight-1, - onTop ? kDbgChangedColor : _bgcolorlo); - s.drawString(_font, buffer, x, y, _colWidth, onTop ? kDbgChangedTextColor : kColor); + s.fillRect(x - 3, y - 1, _colWidth-1, _rowHeight-1, kDbgChangedColor); + s.drawString(_font, buffer, x, y, _colWidth, kDbgChangedTextColor); } else - s.drawString(_font, buffer, x, y, _colWidth, - onTop ? textColor : kColor); + s.drawString(_font, buffer, x, y, _colWidth, textColor); } else { - s.fillRect(x - 3, y - 1, _colWidth-1, _rowHeight-1, onTop ? kBGColorHi : kDlgColor); - s.drawString(_font, buffer, x, y, _colWidth, onTop ? kTextColor : kColor); + s.fillRect(x - 3, y - 1, _colWidth-1, _rowHeight-1, kBGColorHi); + s.drawString(_font, buffer, x, y, _colWidth, kTextColor); } } } diff --git a/src/debugger/gui/TogglePixelWidget.cxx b/src/debugger/gui/TogglePixelWidget.cxx index b7dd5dbdf..1cdb80bae 100644 --- a/src/debugger/gui/TogglePixelWidget.cxx +++ b/src/debugger/gui/TogglePixelWidget.cxx @@ -118,7 +118,6 @@ void TogglePixelWidget::drawWidget(bool hilite) { //cerr << "TogglePixelWidget::drawWidget\n"; FBSurface& s = dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); int row, col; s.frameRect(_x, _y, _w, _h, hilite && isEnabled() && isEditable() ? kWidColorHi : kColor); @@ -146,7 +145,7 @@ void TogglePixelWidget::drawWidget(bool hilite) // Either draw the pixel in given color, or erase (show background) s.fillRect(x - 3, y - 1, _colWidth-1, _rowHeight-1, - _stateList[pos] ? onTop ? _pixelColor : kColor : onTop ? _backgroundColor : kBGColorLo); + _stateList[pos] ? _pixelColor : _backgroundColor); if (_changedList[pos]) s.frameRect(x - 3, y - 1, _colWidth - 1, _rowHeight - 1, kDbgChangedColor); } diff --git a/src/gui/CheckListWidget.cxx b/src/gui/CheckListWidget.cxx index 6d80fcc07..13afb335a 100644 --- a/src/gui/CheckListWidget.cxx +++ b/src/gui/CheckListWidget.cxx @@ -81,7 +81,6 @@ void CheckListWidget::drawWidget(bool hilite) { //cerr << "CheckListWidget::drawWidget\n"; FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); int i, pos, len = int(_list.size()); // Draw a thin frame around the list and to separate columns @@ -112,18 +111,17 @@ void CheckListWidget::drawWidget(bool hilite) } else s.frameRect(_x + r.x() - 3, _y + 1 + _lineHeight * i, - _w - r.x(), _lineHeight, onTop ? kTextColorHi : kColor); + _w - r.x(), _lineHeight, kTextColorHi); } if (_selectedItem == pos && _editMode) { adjustOffset(); - s.drawString(_font, editString(), _x + r.x(), y, r.w(), onTop ? kTextColor : kColor, + s.drawString(_font, editString(), _x + r.x(), y, r.w(), kTextColor, TextAlign::Left, -_editScrollOffset, false); } else - s.drawString(_font, _list[pos], _x + r.x(), y, r.w(), - onTop ? textColor : kColor); + s.drawString(_font, _list[pos], _x + r.x(), y, r.w(), textColor); } // Only draw the caret while editing, and if it's in the current viewport diff --git a/src/gui/ColorWidget.cxx b/src/gui/ColorWidget.cxx index 27f4bc3cf..5f31df35d 100644 --- a/src/gui/ColorWidget.cxx +++ b/src/gui/ColorWidget.cxx @@ -45,7 +45,6 @@ void ColorWidget::setColor(ColorId color) void ColorWidget::drawWidget(bool hilite) { FBSurface& s = dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); if(_framed) { @@ -53,11 +52,11 @@ void ColorWidget::drawWidget(bool hilite) s.frameRect(_x, _y, _w, _h + 1, kColor); // Show the currently selected color - s.fillRect(_x + 1, _y + 1, _w - 2, _h - 1, onTop ? isEnabled() ? _color : kWidColor : kBGColorLo); + s.fillRect(_x + 1, _y + 1, _w - 2, _h - 1, isEnabled() ? _color : kWidColor); } else { - s.fillRect(_x, _y, _w, _h, onTop ? isEnabled() ? _color : kWidColor : kBGColorLo); + s.fillRect(_x, _y, _w, _h, isEnabled() ? _color : kWidColor); } // Cross out the grid? diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index f628ec792..7935f1c95 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -55,7 +55,6 @@ class Dialog : public GuiObject void close(); bool isVisible() const override { return _visible; } - bool isOnTop() const { return true; } // TODO: remove virtual void setPosition(); virtual void drawDialog(); diff --git a/src/gui/EditTextWidget.cxx b/src/gui/EditTextWidget.cxx index 61785bd78..6a0f32877 100644 --- a/src/gui/EditTextWidget.cxx +++ b/src/gui/EditTextWidget.cxx @@ -76,13 +76,12 @@ void EditTextWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount void EditTextWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); // Highlight changes - if(_changed && onTop) + if(_changed) s.fillRect(_x, _y, _w, _h, kDbgChangedColor); else if(!isEditable() || !isEnabled()) - s.fillRect(_x, _y, _w, _h, onTop ? kDlgColor : kBGColorLo); + s.fillRect(_x, _y, _w, _h, kDlgColor); // Draw a thin frame around us. s.frameRect(_x, _y, _w, _h, hilite && isEditable() && isEnabled() ? kWidColorHi : kColor); @@ -90,9 +89,9 @@ void EditTextWidget::drawWidget(bool hilite) // Draw the text adjustOffset(); s.drawString(_font, editString(), _x + _textOfs, _y + 2, getEditRect().w(), getEditRect().h(), - _changed && onTop && isEnabled() + _changed && isEnabled() ? kDbgChangedTextColor - : onTop && isEnabled() ? _textcolor : kColor, + : isEnabled() ? _textcolor : kColor, TextAlign::Left, scrollOffset(), !isEditable()); // Draw the caret and selection diff --git a/src/gui/PopUpWidget.cxx b/src/gui/PopUpWidget.cxx index 45f79e55b..82f4e2596 100644 --- a/src/gui/PopUpWidget.cxx +++ b/src/gui/PopUpWidget.cxx @@ -249,7 +249,6 @@ void PopUpWidget::drawWidget(bool hilite) { //cerr << "PopUpWidget::drawWidget\n"; FBSurface& s = dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); int x = _x + _labelWidth; int w = _w - _labelWidth; @@ -257,7 +256,7 @@ void PopUpWidget::drawWidget(bool hilite) // Draw the label, if any if(_labelWidth > 0) s.drawString(_font, _label, _x, _y + myTextY, _labelWidth, - isEnabled() && onTop ? _textcolor : kColor, TextAlign::Left); + isEnabled() ? _textcolor : kColor, TextAlign::Left); // Draw a thin frame around us. s.frameRect(x, _y, w, _h, isEnabled() && hilite ? kWidColorHi : kColor); @@ -267,12 +266,12 @@ void PopUpWidget::drawWidget(bool hilite) // Fill the background ColorId bgCol = isEditable() ? kWidColor : kDlgColor; s.fillRect(x + 1, _y + 1, w - (_arrowWidth * 2 - 1), _h - 2, - onTop ? _changed ? kDbgChangedColor : bgCol : kDlgColor); + _changed ? kDbgChangedColor : bgCol); s.fillRect(x + w - (_arrowWidth * 2 - 2), _y + 1, (_arrowWidth * 2 - 3), _h - 2, - onTop ? isEnabled() && hilite ? kBtnColorHi : bgCol : kBGColorLo); + isEnabled() && hilite ? kBtnColorHi : bgCol); // Draw an arrow pointing down at the right end to signal this is a dropdown/popup s.drawBitmap(_arrowImg, x + w - (_arrowWidth * 1.5 - 1), _y + myArrowsY + 1, - !(isEnabled() && onTop) ? kColor : kTextColor, _arrowWidth, _arrowHeight); + !isEnabled() ? kColor : kTextColor, _arrowWidth, _arrowHeight); // Draw the selected entry, if any const string& name = editString(); @@ -283,7 +282,7 @@ void PopUpWidget::drawWidget(bool hilite) TextAlign::Right : TextAlign::Left; adjustOffset(); s.drawString(_font, name, x + _textOfs, _y + myTextY, w, - !(isEnabled() && onTop) ? kColor : _changed ? kDbgChangedTextColor : kTextColor, + !isEnabled() ? kColor : _changed ? kDbgChangedTextColor : kTextColor, align, editable ? -_editScrollOffset : 0, !editable); if(editable) diff --git a/src/gui/RomInfoWidget.cxx b/src/gui/RomInfoWidget.cxx index c2068a4d1..325cadfda 100644 --- a/src/gui/RomInfoWidget.cxx +++ b/src/gui/RomInfoWidget.cxx @@ -180,11 +180,9 @@ void RomInfoWidget::parseProperties(const FilesystemNode& node) void RomInfoWidget::drawWidget(bool hilite) { FBSurface& s = dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); - const int yoff = myAvail.h + 10; - s.fillRect(_x+2, _y+2, _w-4, _h-4, onTop ? _bgcolor : _bgcolorlo); + s.fillRect(_x+2, _y+2, _w-4, _h-4, _bgcolor); s.frameRect(_x, _y, _w, _h, kColor); s.frameRect(_x, _y+yoff, _w, _h-yoff, kColor); @@ -206,7 +204,7 @@ void RomInfoWidget::drawWidget(bool hilite) { uInt32 x = _x + ((_w - _font.getStringWidth(mySurfaceErrorMsg)) >> 1); uInt32 y = _y + ((yoff - _font.getLineHeight()) >> 1); - s.drawString(_font, mySurfaceErrorMsg, x, y, _w - 10, onTop ? _textcolor : _shadowcolor); + s.drawString(_font, mySurfaceErrorMsg, x, y, _w - 10, _textcolor); } int xpos = _x + 8, ypos = _y + yoff + 5; @@ -226,7 +224,7 @@ void RomInfoWidget::drawWidget(bool hilite) break; } int lines = s.drawString(_font, info, xpos, ypos, _w - 16, _font.getFontHeight() * 3, - onTop ? _textcolor : _shadowcolor); + _textcolor); ypos += _font.getLineHeight() + (lines - 1) * _font.getFontHeight(); } clearDirty(); diff --git a/src/gui/ScrollBarWidget.cxx b/src/gui/ScrollBarWidget.cxx index 73e89bc78..4bacbeece 100644 --- a/src/gui/ScrollBarWidget.cxx +++ b/src/gui/ScrollBarWidget.cxx @@ -274,9 +274,7 @@ void ScrollBarWidget::recalc() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ScrollBarWidget::drawWidget(bool hilite) { -//cerr << "ScrollBarWidget::drawWidget\n"; FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); int bottomY = _y + _h; bool isSinglePage = (_numEntries <= _entriesPerPage); @@ -290,22 +288,24 @@ void ScrollBarWidget::drawWidget(bool hilite) s.fillRect(_x + 1, _y + 1, _w - 2, _upDownBoxHeight - 2, kScrollColor); s.drawBitmap(_upImg, _x + (_scrollBarWidth - _upDownWidth) / 2, _y + (_upDownBoxHeight - _upDownHeight) / 2, - onTop ? isSinglePage ? kColor : (hilite && _part == Part::UpArrow) ? kWidColor - : kTextColor : kColor, _upDownWidth, _upDownHeight); + isSinglePage ? kColor + : (hilite && _part == Part::UpArrow) ? kWidColor : kTextColor, + _upDownWidth, _upDownHeight); // Down arrow if(hilite && _part == Part::DownArrow) s.fillRect(_x + 1, bottomY - _upDownBoxHeight + 1, _w - 2, _upDownBoxHeight - 2, kScrollColor); s.drawBitmap(_downImg, _x + (_scrollBarWidth - _upDownWidth) / 2, bottomY - _upDownBoxHeight + (_upDownBoxHeight - _upDownHeight) / 2, - onTop ? isSinglePage ? kColor : (hilite && _part == Part::DownArrow) ? - kWidColor : kTextColor : kColor, _upDownWidth, _upDownHeight); + isSinglePage ? kColor + : (hilite && _part == Part::DownArrow) ? kWidColor : kTextColor, + _upDownWidth, _upDownHeight); // Slider if(!isSinglePage) { s.fillRect(_x + 1, _y + _sliderPos - 1, _w - 2, _sliderHeight + 2, - onTop ? (hilite && _part == Part::Slider) ? kScrollColorHi : kScrollColor : kColor); + (hilite && _part == Part::Slider) ? kScrollColorHi : kScrollColor); } clearDirty(); } diff --git a/src/gui/StringListWidget.cxx b/src/gui/StringListWidget.cxx index 536240b8a..9173d6276 100644 --- a/src/gui/StringListWidget.cxx +++ b/src/gui/StringListWidget.cxx @@ -95,23 +95,22 @@ bool StringListWidget::changedToolTip(const Common::Point& oldPos, void StringListWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); int i, pos, len = int(_list.size()); // Draw a thin frame around the list. - s.frameRect(_x, _y, _w + 1, _h, onTop && hilite && _hilite ? kWidColorHi : kColor); + s.frameRect(_x, _y, _w + 1, _h, hilite && _hilite ? kWidColorHi : kColor); if (!isEnabled()) - s.fillRect(_x + 1, _y + 1, _w - 1, _h - 2, onTop ? kDlgColor : kBGColorLo); + s.fillRect(_x + 1, _y + 1, _w - 1, _h - 2, kDlgColor); // Draw the list items for (i = 0, pos = _currentPos; i < _rows && pos < len; i++, pos++) { const int y = _y + 2 + _lineHeight * i; - ColorId textColor = onTop ? kTextColor : kShadowColor; + ColorId textColor = kTextColor; // Draw the selected item inverted, on a highlighted background. - if (onTop && _selectedItem == pos && _hilite) + if (_selectedItem == pos && _hilite) { if(_hasFocus && !_editMode) { diff --git a/src/gui/TabWidget.cxx b/src/gui/TabWidget.cxx index 28b383f6c..3c7d48258 100644 --- a/src/gui/TabWidget.cxx +++ b/src/gui/TabWidget.cxx @@ -265,36 +265,34 @@ void TabWidget::drawWidget(bool hilite) if(isDirty()) { FBSurface& s = dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); // Iterate over all tabs and draw them int i, x = _x + kTabLeftOffset; for(i = 0; i < int(_tabs.size()); ++i) { int tabWidth = _tabs[i].tabWidth ? _tabs[i].tabWidth : _tabWidth; - ColorId fontcolor = _tabs[i].enabled && onTop ? kTextColor : kColor; + ColorId fontcolor = _tabs[i].enabled ? kTextColor : kColor; int yOffset = (i == _activeTab) ? 0 : 1; s.fillRect(x, _y + 1, tabWidth, _tabHeight - 1, (i == _activeTab) - ? onTop ? kDlgColor : kBGColorLo - : onTop ? kBGColorHi : kDlgColor); // ? kWidColor : kDlgColor + ? kDlgColor : kBGColorHi); // ? kWidColor : kDlgColor s.drawString(_font, _tabs[i].title, x + kTabPadding + yOffset, _y + yOffset + (_tabHeight - _lineHeight - 1), tabWidth - 2 * kTabPadding, fontcolor, TextAlign::Center); if(i == _activeTab) { - s.hLine(x, _y, x + tabWidth - 1, onTop ? kWidColor : kDlgColor); - s.vLine(x + tabWidth, _y + 1, _y + _tabHeight - 1, onTop ? kBGColorLo : kColor); + s.hLine(x, _y, x + tabWidth - 1, kWidColor); + s.vLine(x + tabWidth, _y + 1, _y + _tabHeight - 1, kBGColorLo); } else - s.hLine(x, _y + _tabHeight, x + tabWidth, onTop ? kWidColor : kDlgColor); + s.hLine(x, _y + _tabHeight, x + tabWidth, kWidColor); x += tabWidth + kTabSpacing; } // fill empty right space - s.hLine(x - kTabSpacing + 1, _y + _tabHeight, _x + _w - 1, onTop ? kWidColor : kDlgColor); - s.hLine(_x, _y + _h - 1, _x + _w - 1, onTop ? kBGColorLo : kColor); + s.hLine(x - kTabSpacing + 1, _y + _tabHeight, _x + _w - 1, kWidColor); + s.hLine(_x, _y + _h - 1, _x + _w - 1, kBGColorLo); clearDirty(); // Make all child widgets of currently active tab dirty diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 23c72760d..1856c3f4b 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -101,8 +101,6 @@ void Widget::draw() //cerr << "w"; FBSurface& s = _boss->dialog().surface(); - - bool onTop = _boss->dialog().isOnTop(); int oldX = _x, oldY = _y; // Account for our relative position in the dialog @@ -118,9 +116,7 @@ void Widget::draw() x++; y++; w -= 2; h -= 2; } if(hasBackground()) - s.fillRect(x, y, w, h, !onTop - ? _bgcolorlo - : (_flags & Widget::FLAG_HILITED) && isEnabled() + s.fillRect(x, y, w, h, (_flags & Widget::FLAG_HILITED) && isEnabled() ? _bgcolorhi : _bgcolor); else s.invalidateRect(x, y, w, h); @@ -129,9 +125,7 @@ void Widget::draw() // Draw border if(hasBorder()) { - s.frameRect(_x, _y, _w, _h, !onTop - ? kColor - : (_flags & Widget::FLAG_HILITED) && isEnabled() + s.frameRect(_x, _y, _w, _h, (_flags & Widget::FLAG_HILITED) && isEnabled() ? kWidColorHi : kColor); _x += 4; _y += 4; @@ -275,7 +269,6 @@ Widget* Widget::setFocusForChain(GuiObject* boss, WidgetArray& arr, FBSurface& s = boss->dialog().surface(); int size = int(arr.size()), pos = -1; Widget* tmp; - bool onTop = boss->dialog().isOnTop(); for(int i = 0; i < size; ++i) { @@ -300,9 +293,7 @@ Widget* Widget::setFocusForChain(GuiObject* boss, WidgetArray& arr, else tmp->_hasFocus = false; - s.frameRect(x, y, w, h, onTop ? kDlgColor : kBGColorLo); - - //tmp->setDirty(); + s.frameRect(x, y, w, h, kDlgColor); } } @@ -355,10 +346,7 @@ Widget* Widget::setFocusForChain(GuiObject* boss, WidgetArray& arr, tmp->setFlags(Widget::FLAG_HILITED); } - if (onTop) - s.frameRect(x, y, w, h, kWidFrameColor, FrameStyle::Dashed); - - //tmp->setDirty(); + s.frameRect(x, y, w, h, kWidFrameColor, FrameStyle::Dashed); return tmp; } @@ -439,9 +427,9 @@ void StaticTextWidget::handleMouseLeft() void StaticTextWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); + s.drawString(_font, _label, _x, _y, _w, - isEnabled() && onTop ? _textcolor : kColor, _align, 0, true, _shadowcolor); + isEnabled() ? _textcolor : kColor, _align, 0, true, _shadowcolor); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -558,17 +546,16 @@ void ButtonWidget::setBitmap(const uInt32* bitmap, int bmw, int bmh) void ButtonWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); - s.frameRect(_x, _y, _w, _h, !onTop ? kShadowColor : hilite && isEnabled() ? kBtnBorderColorHi : kBtnBorderColor); + s.frameRect(_x, _y, _w, _h, hilite && isEnabled() ? kBtnBorderColorHi : kBtnBorderColor); if (!_useBitmap) s.drawString(_font, _label, _x, _y + (_h - _lineHeight)/2 + 1, _w, - !(isEnabled() && onTop) ? _textcolorlo : + !isEnabled() ? _textcolorlo : hilite ? _textcolorhi : _textcolor, _align); else s.drawBitmap(_bitmap, _x + (_w - _bmw) / 2, _y + (_h - _bmh) / 2, - !(isEnabled() && onTop) ? _textcolorlo : + !isEnabled() ? _textcolorlo : hilite ? _textcolorhi : _textcolor, _bmw, _bmh); } @@ -703,21 +690,19 @@ void CheckboxWidget::setState(bool state, bool changed) void CheckboxWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); - bool onTop = _boss->dialog().isOnTop(); if(_drawBox) - s.frameRect(_x, _y + _boxY, _boxSize, _boxSize, onTop && hilite && isEnabled() && isEditable() ? kWidColorHi : kColor); + s.frameRect(_x, _y + _boxY, _boxSize, _boxSize, hilite && isEnabled() && isEditable() ? kWidColorHi : kColor); // Do we draw a square or cross? s.fillRect(_x + 1, _y + _boxY + 1, _boxSize - 2, _boxSize - 2, - _changed ? onTop ? kDbgChangedColor : kDlgColor : - isEnabled() && onTop ? _bgcolor : kDlgColor); + _changed ? kDbgChangedColor : isEnabled() ? _bgcolor : kDlgColor); if(_state) - s.drawBitmap(_img, _x + 2, _y + _boxY + 2, onTop && isEnabled() ? hilite && isEditable() ? kWidColorHi : kCheckColor + s.drawBitmap(_img, _x + 2, _y + _boxY + 2, isEnabled() ? hilite && isEditable() ? kWidColorHi : kCheckColor : kColor, _boxSize - 4); // Finally draw the label s.drawString(_font, _label, _x + prefixSize(_font), _y + _textY, _w, - onTop && isEnabled() ? kTextColor : kColor); + isEnabled() ? kTextColor : kColor); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 760f32c5c2c0ba11d2aec92ffa09e28182fb51d6 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Thu, 19 Nov 2020 23:18:28 +0100 Subject: [PATCH 076/107] changed y-position displayed in tooltip to scanline number --- src/debugger/gui/TiaOutputWidget.cxx | 7 ++++--- src/debugger/gui/TiaZoomWidget.cxx | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/debugger/gui/TiaOutputWidget.cxx b/src/debugger/gui/TiaOutputWidget.cxx index 6d2a1b9cb..424efd0d1 100644 --- a/src/debugger/gui/TiaOutputWidget.cxx +++ b/src/debugger/gui/TiaOutputWidget.cxx @@ -104,14 +104,14 @@ void TiaOutputWidget::saveSnapshot(int execDepth, const string& execPrefix) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TiaOutputWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) -{ +{ if(b == MouseButton::LEFT) myZoom->setPos(x, y); // Grab right mouse button for command context menu else if(b == MouseButton::RIGHT) { myClickX = x; - myClickY = y; + myClickY = y - 1; dialog().tooltip().hide(); // Add menu at current x,y mouse location @@ -184,6 +184,7 @@ string TiaOutputWidget::getToolTip(const Common::Point& pos) const if(idx.x < 0) return EmptyString; + const uInt32 startLine = instance().console().tia().startLine(); const uInt32 height = instance().console().tia().height(); // limit to 274 lines (PAL default without scaling) const uInt32 yStart = height <= FrameManager::Metrics::baseHeightPAL @@ -194,7 +195,7 @@ string TiaOutputWidget::getToolTip(const Common::Point& pos) const buf << _toolTipText << "X: #" << idx.x - << "\nY: #" << idx.y + << "\nY: #" << idx.y + startLine << "\nC: $" << Common::Base::toString(tiaOutputBuffer[i], Common::Base::Fmt::_16); return buf.str(); diff --git a/src/debugger/gui/TiaZoomWidget.cxx b/src/debugger/gui/TiaZoomWidget.cxx index 3cbbad8e2..3622c11ac 100644 --- a/src/debugger/gui/TiaZoomWidget.cxx +++ b/src/debugger/gui/TiaZoomWidget.cxx @@ -117,7 +117,7 @@ void TiaZoomWidget::recalc() void TiaZoomWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) { myClickX = x; - myClickY = y; + myClickY = y - 1; // Button 1 is for 'drag'/movement of the image // Button 2 is for context menu @@ -148,7 +148,7 @@ void TiaZoomWidget::handleMouseWheel(int x, int y, int direction) // zoom towards mouse position myClickX = x; - myClickY = y; + myClickY = y - 1; if(direction > 0) { @@ -167,6 +167,7 @@ void TiaZoomWidget::handleMouseMoved(int x, int y) { if(myMouseMoving) { + y--; int diffx = x + myOffXLo - myClickX; int diffy = y + myOffYLo - myClickY; @@ -302,12 +303,13 @@ string TiaZoomWidget::getToolTip(const Common::Point& pos) const return EmptyString; const Int32 i = idx.x + idx.y * instance().console().tia().width(); + const uInt32 startLine = instance().console().tia().startLine(); uInt8* tiaOutputBuffer = instance().console().tia().outputBuffer(); ostringstream buf; buf << _toolTipText << "X: #" << idx.x - << "\nY: #" << idx.y + << "\nY: #" << idx.y + startLine << "\nC: $" << Common::Base::toString(tiaOutputBuffer[i], Common::Base::Fmt::_16); return buf.str(); From 8d27e645734b09c58b9f7ef66e0118ba44c3d2ae Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 20 Nov 2020 10:11:40 +0100 Subject: [PATCH 077/107] improved tooltips hiding added tooltip to breakpoint/trap status added tooltip to search/compare buttons and dialogs added tooltips to data operation buttons --- src/debugger/Debugger.cxx | 4 +++- src/debugger/Debugger.hxx | 3 ++- src/debugger/gui/CpuWidget.cxx | 4 ++-- src/debugger/gui/DataGridOpsWidget.cxx | 9 ++++++++- src/debugger/gui/DataGridWidget.cxx | 1 + src/debugger/gui/DebuggerDialog.cxx | 1 + src/debugger/gui/RamWidget.cxx | 6 ++++++ src/debugger/gui/TiaOutputWidget.cxx | 4 +--- src/debugger/gui/TiaZoomWidget.cxx | 1 - src/emucore/DispatchResult.cxx | 4 +++- src/emucore/DispatchResult.hxx | 8 ++++++-- src/emucore/M6502.cxx | 14 ++++++++------ src/emucore/OSystem.cxx | 3 ++- src/gui/Dialog.cxx | 2 ++ src/gui/DialogContainer.cxx | 5 +++++ src/gui/InputTextDialog.cxx | 14 +++++++++++--- src/gui/InputTextDialog.hxx | 2 ++ src/gui/LauncherDialog.cxx | 1 - src/gui/PopUpWidget.cxx | 2 -- 19 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/debugger/Debugger.cxx b/src/debugger/Debugger.cxx index e363a0b82..abfc63ac1 100644 --- a/src/debugger/Debugger.cxx +++ b/src/debugger/Debugger.cxx @@ -118,7 +118,8 @@ FBInitStatus Debugger::initializeVideo() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool Debugger::start(const string& message, int address, bool read) +bool Debugger::start(const string& message, int address, bool read, + const string& toolTip) { if(myOSystem.eventHandler().enterDebugMode()) { @@ -129,6 +130,7 @@ bool Debugger::start(const string& message, int address, bool read) if(address > -1) buf << cartDebug().getLabel(address, read, 4); myDialog->message().setText(buf.str()); + myDialog->message().setToolTip(toolTip); return true; } return false; diff --git a/src/debugger/Debugger.hxx b/src/debugger/Debugger.hxx index e7096cd4d..6a53238bb 100644 --- a/src/debugger/Debugger.hxx +++ b/src/debugger/Debugger.hxx @@ -97,7 +97,8 @@ class Debugger : public DialogContainer @param message Message to display when entering debugger @param address An address associated with the message */ - bool start(const string& message = "", int address = -1, bool read = true); + bool start(const string& message = "", int address = -1, bool read = true, + const string& toolTip = ""); bool startWithFatalError(const string& message = ""); /** diff --git a/src/debugger/gui/CpuWidget.cxx b/src/debugger/gui/CpuWidget.cxx index a8719f448..1069e8001 100644 --- a/src/debugger/gui/CpuWidget.cxx +++ b/src/debugger/gui/CpuWidget.cxx @@ -91,7 +91,7 @@ CpuWidget::CpuWidget(GuiObject* boss, const GUI::Font& lfont, const GUI::Font& n for(int i = 0; i < 4; ++i) { myCpuDataSrc[i] = new EditTextWidget(boss, nfont, xpos, src_y, src_w, fontHeight + 1); - myCpuDataSrc[i]->setToolTip("Source label of last load into " + labels[i] + "."); + myCpuDataSrc[i]->setToolTip("Source label of last read for " + labels[i] + "."); myCpuDataSrc[i]->setEditable(false, true); src_y += fontHeight + 2; } @@ -140,7 +140,7 @@ CpuWidget::CpuWidget(GuiObject* boss, const GUI::Font& lfont, const GUI::Font& n xpos = myCpuDataSrc[0]->getLeft(); new StaticTextWidget(boss, lfont, xpos - fontWidth * 4.5, ypos + 2, "Dest"); myCpuDataDest = new EditTextWidget(boss, nfont, xpos, ypos, src_w, fontHeight + 1); - myCpuDataDest->setToolTip("Destination label of last store."); + myCpuDataDest->setToolTip("Destination label of last write."); myCpuDataDest->setEditable(false, true); _h = ypos + myPSRegister->getHeight() - y; diff --git a/src/debugger/gui/DataGridOpsWidget.cxx b/src/debugger/gui/DataGridOpsWidget.cxx index 102d7a94d..2a62ec60c 100644 --- a/src/debugger/gui/DataGridOpsWidget.cxx +++ b/src/debugger/gui/DataGridOpsWidget.cxx @@ -33,31 +33,38 @@ DataGridOpsWidget::DataGridOpsWidget(GuiObject* boss, const GUI::Font& font, xpos = x; ypos = y; _zeroButton = new ButtonWidget(boss, font, xpos, ypos, bwidth, bheight, "0", kDGZeroCmd); - + _zeroButton->setToolTip("Zero currently selected value"); + ypos += bheight + space; _invButton = new ButtonWidget(boss, font, xpos, ypos, bwidth, bheight, "Inv", kDGInvertCmd); + _invButton->setToolTip("Invert currently selected value"); ypos += bheight + space; _incButton = new ButtonWidget(boss, font, xpos, ypos, bwidth, bheight, "++", kDGIncCmd); + _incButton->setToolTip("Increase currently selected value."); ypos += bheight + space; _shiftLeftButton = new ButtonWidget(boss, font, xpos, ypos, bwidth, bheight, "<<", kDGShiftLCmd); + _shiftLeftButton->setToolTip("Shift currently selected value left"); // Move to next column, skip a row xpos = x + bwidth + space; ypos = y + bheight + space; _negButton = new ButtonWidget(boss, font, xpos, ypos, bwidth, bheight, "Neg", kDGNegateCmd); + _negButton->setToolTip("Negate currently selected value"); ypos += bheight + space; _decButton = new ButtonWidget(boss, font, xpos, ypos, bwidth, bheight, "--", kDGDecCmd); + _decButton->setToolTip("Decrease currently selected value"); ypos += bheight + space; _shiftRightButton = new ButtonWidget(boss, font, xpos, ypos, bwidth, bheight, ">>", kDGShiftRCmd); + _shiftRightButton->setToolTip("Shift currently selected value right"); // Calculate real dimensions _w = 2 * (bwidth+space); diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index 81840147a..20489912c 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -713,6 +713,7 @@ void DataGridWidget::startEditMode() { if (isEditable() && !_editMode && _selectedItem >= 0) { + dialog().tooltip().hide(); enableEditMode(true); setText("", true); // Erase current entry when starting editing } diff --git a/src/debugger/gui/DebuggerDialog.cxx b/src/debugger/gui/DebuggerDialog.cxx index 07e530ac6..b8a28f579 100644 --- a/src/debugger/gui/DebuggerDialog.cxx +++ b/src/debugger/gui/DebuggerDialog.cxx @@ -94,6 +94,7 @@ void DebuggerDialog::loadConfig() myRomTab->loadConfig(); myMessageBox->setText(""); + myMessageBox->setToolTip(""); } void DebuggerDialog::saveConfig() diff --git a/src/debugger/gui/RamWidget.cxx b/src/debugger/gui/RamWidget.cxx index 7f651910a..4e097d9be 100644 --- a/src/debugger/gui/RamWidget.cxx +++ b/src/debugger/gui/RamWidget.cxx @@ -78,18 +78,21 @@ RamWidget::RamWidget(GuiObject* boss, const GUI::Font& lfont, const GUI::Font& n by += bheight + VGAP * 6; mySearchButton = new ButtonWidget(boss, lfont, bx, by, bwidth, bheight, "Search" + ELLIPSIS, kSearchCmd); + mySearchButton->setToolTip("Search and highlight found values."); wid.push_back(mySearchButton); mySearchButton->setTarget(this); by += bheight + VGAP; myCompareButton = new ButtonWidget(boss, lfont, bx, by, bwidth, bheight, "Compare" + ELLIPSIS, kCmpCmd); + myCompareButton->setToolTip("Compare highlighted values."); wid.push_back(myCompareButton); myCompareButton->setTarget(this); by += bheight + VGAP; myRestartButton = new ButtonWidget(boss, lfont, bx, by, bwidth, bheight, "Reset", kRestartCmd); + myRestartButton->setToolTip("Reset search/compare mode."); wid.push_back(myRestartButton); myRestartButton->setTarget(this); @@ -366,6 +369,9 @@ void RamWidget::showInputBox(int cmd) myInputBox->show(x, y, dialog().surface().dstRect()); myInputBox->setText(""); myInputBox->setMessage(""); + myInputBox->setToolTip(cmd == kSValEntered + ? "Enter search value (leave blank for all)." + : "Enter relative or absolute value\nto compare with searched values."); myInputBox->setFocus(0); myInputBox->setEmitSignal(cmd); myInputBox->setTitle(cmd == kSValEntered ? "Search" : "Compare"); diff --git a/src/debugger/gui/TiaOutputWidget.cxx b/src/debugger/gui/TiaOutputWidget.cxx index 424efd0d1..d101fa674 100644 --- a/src/debugger/gui/TiaOutputWidget.cxx +++ b/src/debugger/gui/TiaOutputWidget.cxx @@ -23,7 +23,6 @@ #include "Widget.hxx" #include "GuiObject.hxx" #include "Dialog.hxx" -#include "ToolTip.hxx" #include "ContextMenu.hxx" #include "TiaZoomWidget.hxx" #include "Debugger.hxx" @@ -104,7 +103,7 @@ void TiaOutputWidget::saveSnapshot(int execDepth, const string& execPrefix) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TiaOutputWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) -{ +{ if(b == MouseButton::LEFT) myZoom->setPos(x, y); // Grab right mouse button for command context menu @@ -113,7 +112,6 @@ void TiaOutputWidget::handleMouseDown(int x, int y, MouseButton b, int clickCoun myClickX = x; myClickY = y - 1; - dialog().tooltip().hide(); // Add menu at current x,y mouse location myMenu->show(x + getAbsX(), y + getAbsY(), dialog().surface().dstRect()); } diff --git a/src/debugger/gui/TiaZoomWidget.cxx b/src/debugger/gui/TiaZoomWidget.cxx index 3622c11ac..af8e468dd 100644 --- a/src/debugger/gui/TiaZoomWidget.cxx +++ b/src/debugger/gui/TiaZoomWidget.cxx @@ -129,7 +129,6 @@ void TiaZoomWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) } else if(b == MouseButton::RIGHT) { - dialog().tooltip().hide(); // Add menu at current x,y mouse location myMenu->show(x + getAbsX(), y + getAbsY(), dialog().surface().dstRect()); } diff --git a/src/emucore/DispatchResult.cxx b/src/emucore/DispatchResult.cxx index 20a208a56..9ba5326ca 100644 --- a/src/emucore/DispatchResult.cxx +++ b/src/emucore/DispatchResult.cxx @@ -31,11 +31,13 @@ void DispatchResult::setOk(uInt64 cycles) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void DispatchResult::setDebugger(uInt64 cycles, const string& message, int address, bool wasReadTrap) +void DispatchResult::setDebugger(uInt64 cycles, const string& message, + const string& tooltip, int address, bool wasReadTrap) { myStatus = Status::debugger; myCycles = cycles; myMessage = message; + myToolTip = tooltip; myAddress = address; myWasReadTrap = wasReadTrap; } diff --git a/src/emucore/DispatchResult.hxx b/src/emucore/DispatchResult.hxx index 6d76cf5ce..38e80a9c5 100644 --- a/src/emucore/DispatchResult.hxx +++ b/src/emucore/DispatchResult.hxx @@ -37,12 +37,14 @@ class DispatchResult bool wasReadTrap() const { assertStatus(Status::debugger); return myWasReadTrap; } + const string& getToolTip() const { assertStatus(Status::debugger, Status::fatal); return myToolTip; } + bool isSuccess() const; void setOk(uInt64 cycles); - void setDebugger(uInt64 cycles, const string& message = "", int address = -1, - bool wasReadTrap = true); + void setDebugger(uInt64 cycles, const string& message = "", + const string& tooltip = "", int address = -1, bool wasReadTrap = true); void setFatal(uInt64 cycles); @@ -73,6 +75,8 @@ class DispatchResult int myAddress{0}; bool myWasReadTrap{false}; + + string myToolTip; }; #endif // DISPATCH_RESULT_HXX diff --git a/src/emucore/M6502.cxx b/src/emucore/M6502.cxx index 86b050dfb..295697af2 100644 --- a/src/emucore/M6502.cxx +++ b/src/emucore/M6502.cxx @@ -243,7 +243,9 @@ inline void M6502::_execute(uInt64 cycles, DispatchResult& result) myJustHitReadTrapFlag = myJustHitWriteTrapFlag = false; myLastBreakCycle = mySystem->cycles(); - result.setDebugger(currentCycles, myHitTrapInfo.message, myHitTrapInfo.address, read); + result.setDebugger(currentCycles, myHitTrapInfo.message, + read ? "Read trap" : "Write trap", + myHitTrapInfo.address, read); return; } @@ -264,7 +266,7 @@ inline void M6502::_execute(uInt64 cycles, DispatchResult& result) ostringstream msg; msg << "BP: $" << Common::Base::HEX4 << PC << ", bank #" << std::dec << int(bank); - result.setDebugger(currentCycles, msg.str()); + result.setDebugger(currentCycles, msg.str(), "Breakpoint"); } return; } @@ -278,7 +280,7 @@ inline void M6502::_execute(uInt64 cycles, DispatchResult& result) msg << "CBP[" << Common::Base::HEX2 << cond << "]: " << myCondBreakNames[cond]; myLastBreakCycle = mySystem->cycles(); - result.setDebugger(currentCycles, msg.str()); + result.setDebugger(currentCycles, msg.str(), "Conditional breakpoint"); return; } } @@ -327,7 +329,7 @@ inline void M6502::_execute(uInt64 cycles, DispatchResult& result) { ostringstream msg; msg << "RWP[@ $" << Common::Base::HEX4 << rwpAddr << "]: "; - result.setDebugger(currentCycles, msg.str(), oldPC); + result.setDebugger(currentCycles, msg.str(), "Read from write port", oldPC); return; } } @@ -339,7 +341,7 @@ inline void M6502::_execute(uInt64 cycles, DispatchResult& result) { ostringstream msg; msg << "WRP[@ $" << Common::Base::HEX4 << wrpAddr << "]: "; - result.setDebugger(currentCycles, msg.str(), oldPC); + result.setDebugger(currentCycles, msg.str(), "Write to read port", oldPC); return; } } @@ -348,7 +350,7 @@ inline void M6502::_execute(uInt64 cycles, DispatchResult& result) myExecutionStatus |= FatalErrorBit; result.setMessage(e.what()); } catch (const EmulationWarning& e) { - result.setDebugger(currentCycles, e.what(), PC); + result.setDebugger(currentCycles, e.what(), "Emulation exception", PC); return; } diff --git a/src/emucore/OSystem.cxx b/src/emucore/OSystem.cxx index e659c1e5c..b8f8842e6 100644 --- a/src/emucore/OSystem.cxx +++ b/src/emucore/OSystem.cxx @@ -775,7 +775,8 @@ double OSystem::dispatchEmulation(EmulationWorker& emulationWorker) myDebugger->start( dispatchResult.getMessage(), dispatchResult.getAddress(), - dispatchResult.wasReadTrap() + dispatchResult.wasReadTrap(), + dispatchResult.getToolTip() ); #endif diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 1b58d7729..392f2b625 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -512,6 +512,8 @@ void Dialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated) { Event::Type e = Event::NoType; + tooltip().hide(); + // FIXME - I don't think this will compile! #if defined(RETRON77) // special keys used for R77 diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index fcdac91a9..0a6730773 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -17,6 +17,7 @@ #include "OSystem.hxx" #include "Dialog.hxx" +#include "ToolTip.hxx" #include "Stack.hxx" #include "EventHandler.hxx" #include "FrameBuffer.hxx" @@ -159,6 +160,10 @@ int DialogContainer::addDialog(Dialog* d) "Unable to show dialog box; FIX THE CODE", MessagePosition::BottomCenter, true); else { + // Close all open tooltips + if(!myDialogStack.empty()) + myDialogStack.top()->tooltip().hide(); + d->setDirty(); myDialogStack.push(d); } diff --git a/src/gui/InputTextDialog.cxx b/src/gui/InputTextDialog.cxx index 54b656a67..4c34de23e 100644 --- a/src/gui/InputTextDialog.cxx +++ b/src/gui/InputTextDialog.cxx @@ -81,9 +81,10 @@ void InputTextDialog::initialize(const GUI::Font& lfont, const GUI::Font& nfont, for(i = 0; i < labels.size(); ++i) { xpos = HBORDER; - new StaticTextWidget(this, lfont, xpos, ypos + 2, - lwidth, fontHeight, - labels[i], TextAlign::Left); + StaticTextWidget* s = new StaticTextWidget(this, lfont, xpos, ypos + 2, + lwidth, fontHeight, + labels[i]); + myLabel.push_back(s); xpos += lwidth + fontWidth; EditTextWidget* w = new EditTextWidget(this, nfont, xpos, ypos, @@ -177,6 +178,13 @@ void InputTextDialog::setTextFilter(const EditableWidget::TextFilter& f, int idx myInput[idx]->setTextFilter(f); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void InputTextDialog::setToolTip(const string& str, int idx) +{ + if(uInt32(idx) < myLabel.size()) + myLabel[idx]->setToolTip(str); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void InputTextDialog::setFocus(int idx) { diff --git a/src/gui/InputTextDialog.hxx b/src/gui/InputTextDialog.hxx index a06ca4446..059c7ada7 100644 --- a/src/gui/InputTextDialog.hxx +++ b/src/gui/InputTextDialog.hxx @@ -46,6 +46,7 @@ class InputTextDialog : public Dialog, public CommandSender void setText(const string& str, int idx = 0); void setTextFilter(const EditableWidget::TextFilter& f, int idx = 0); + void setToolTip(const string& str, int idx = 0); void setEmitSignal(int cmd) { myCmd = cmd; } void setMessage(const string& title); @@ -61,6 +62,7 @@ class InputTextDialog : public Dialog, public CommandSender void setPosition() override; private: + vector myLabel; vector myInput; StaticTextWidget* myMessage{nullptr}; diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 405be5745..5092299ec 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -568,7 +568,6 @@ void LauncherDialog::handleMouseDown(int x, int y, MouseButton b, int clickCount // Grab right mouse button for context menu, send left to base class if(b == MouseButton::RIGHT) { - dialog().tooltip().hide(); // Dynamically create context menu for ROM list options VariantList items; diff --git a/src/gui/PopUpWidget.cxx b/src/gui/PopUpWidget.cxx index 82f4e2596..eb35ed9c3 100644 --- a/src/gui/PopUpWidget.cxx +++ b/src/gui/PopUpWidget.cxx @@ -21,7 +21,6 @@ #include "Font.hxx" #include "ContextMenu.hxx" #include "Dialog.hxx" -#include "ToolTip.hxx" #include "DialogContainer.hxx" #include "PopUpWidget.hxx" @@ -124,7 +123,6 @@ void PopUpWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount) { if(isEnabled() && !myMenu->isVisible()) { - dialog().tooltip().hide(); // Add menu just underneath parent widget myMenu->show(getAbsX() + _labelWidth, getAbsY() + getHeight(), dialog().surface().dstRect(), myMenu->getSelected()); From 095d83d33548c040576189f510ab0c4be59b46c7 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 20 Nov 2020 10:31:28 +0100 Subject: [PATCH 078/107] updated changes and WhatsNewDialog --- Changes.txt | 10 +++++++++- src/gui/WhatsNewDialog.cxx | 17 ++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Changes.txt b/Changes.txt index 5ae5b0460..4d5776bcb 100644 --- a/Changes.txt +++ b/Changes.txt @@ -14,7 +14,15 @@ 6.4 to 6.5 (December XX, 2020) - * Enhanced cut/copy/paste to allow selecting text (TODO: PromptWidget, doc) + * Enhanced cut/copy/paste for text editing (TODO: PromptWidget) + + * Added undo and redo to text editing (TODO: PromptWidget) + + * Added static tooltips to some UI items + + * Added dynamic tooltips to most debugger items + + * Increased sample size for CDFJ+ -Have fun! diff --git a/src/gui/WhatsNewDialog.cxx b/src/gui/WhatsNewDialog.cxx index 825ed43fe..e94e432a2 100644 --- a/src/gui/WhatsNewDialog.cxx +++ b/src/gui/WhatsNewDialog.cxx @@ -43,18 +43,13 @@ WhatsNewDialog::WhatsNewDialog(OSystem& osystem, DialogContainer& parent, const max_w, max_h); #if defined(RETRON77) - add(ypos, "fixed CDF cartridges crash"); - add(ypos, "fixed crash with SaveKey ROMs (EEPROM file issues)"); - add(ypos, "fixed bug with launcher not remembering last selected ROM"); + add(ypos, "increased sample size for CDFJ+"); + add(ypos, "fixed navigation bug in Video & Audio settings dialog"); #else - add(ypos, "added basic text cut/copy/paste from/to UI"); - add(ypos, "added color parameters to 'Custom' palette"); - add(ypos, "improved AtariVox-USB adaptor autodetection"); - add(ypos, "fixed fullscreen mode, aspect correction and pixel-exact snapshots"); - add(ypos, "fixed reduced ARM emulation performance for CDF ROMs"); - add(ypos, "fixed crash with SaveKey ROMs (EEPROM file issues)"); - add(ypos, "fixed Atari mouse autodetection"); - add(ypos, "fixed bug with launcher not remembering last selected ROM"); + add(ypos, "enhanced cut/copy/paste for text editing"); + add(ypos, "added undo and redo to text editing"); + add(ypos, "added tooltips to many UI items"); + add(ypos, "increased sample size for CDFJ+"); add(ypos, ELLIPSIS + " (for a complete list see 'docs/Changes.txt')"); #endif From 654ca21817b199d8c68a94de1373437ffac196df Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Fri, 20 Nov 2020 10:44:32 -0330 Subject: [PATCH 079/107] Don't forget about Mac for a tooltip. --- src/gui/LauncherDialog.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 5092299ec..cbab575bd 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -183,7 +183,6 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, #ifndef BSPF_MACOS myStartButton = new ButtonWidget(this, font, xpos, ypos, (buttonWidth + 0) / 4, buttonHeight, "Select", kLoadROMCmd); - myStartButton->setToolTip("Start emulation of selected ROM."); wid.push_back(myStartButton); xpos += (buttonWidth + 0) / 4 + BUTTON_GAP; @@ -220,6 +219,7 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, "Select", kLoadROMCmd); wid.push_back(myStartButton); #endif + myStartButton->setToolTip("Start emulation of selected ROM."); } if(myUseMinimalUI) // Highlight 'Rom Listing' mySelectedItem = 0; From 2eccae50a0951779c9b5a2610704b480f6030c79 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 20 Nov 2020 20:12:30 +0100 Subject: [PATCH 080/107] added tooltips to DeveloperDialog --- src/gui/DeveloperDialog.cxx | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/gui/DeveloperDialog.cxx b/src/gui/DeveloperDialog.cxx index 6c877351a..e7d4f1e47 100644 --- a/src/gui/DeveloperDialog.cxx +++ b/src/gui/DeveloperDialog.cxx @@ -115,10 +115,11 @@ void DeveloperDialog::addEmulationTab(const GUI::Font& font) "Console info overlay"); wid.push_back(myFrameStatsWidget); - myDetectedInfoWidget = new CheckboxWidget(myTab, font, myFrameStatsWidget->getRight() + fontWidth * 2.5, ypos + 1, "Detected settings info"); + myDetectedInfoWidget->setToolTip("Display detected controllers, bankswitching\n" + "and TV types at ROM start."); wid.push_back(myDetectedInfoWidget); ypos += lineHeight + VGAP; @@ -131,6 +132,8 @@ void DeveloperDialog::addEmulationTab(const GUI::Font& font) myConsoleWidget = new PopUpWidget(myTab, font, HBORDER + INDENT * 1, ypos, pwidth, lineHeight, items, "Console ", lwidth, kConsole); + myConsoleWidget->setToolTip("Emulate Color/B&W/Pause key and zero\n" + "page RAM initialization differenly."); wid.push_back(myConsoleWidget); ypos += lineHeight + VGAP; @@ -141,6 +144,8 @@ void DeveloperDialog::addEmulationTab(const GUI::Font& font) myRandomBankWidget = new CheckboxWidget(myTab, font, HBORDER + INDENT * 2, ypos + 1, "Random startup bank"); + myRandomBankWidget->setToolTip("Randomize the startup bank for\n" + "most classic bankswitching types."); wid.push_back(myRandomBankWidget); ypos += lineHeight + VGAP; @@ -168,17 +173,23 @@ void DeveloperDialog::addEmulationTab(const GUI::Font& font) // How to handle undriven TIA pins myUndrivenPinsWidget = new CheckboxWidget(myTab, font, HBORDER + INDENT * 1, ypos + 1, "Drive unused TIA pins randomly on a read/peek"); + myUndrivenPinsWidget->setToolTip("Read TIA pins random instead of last databus values.\n" + "Helps detecting missing '#' for immediate loads."); wid.push_back(myUndrivenPinsWidget); ypos += lineHeight + VGAP; #ifdef DEBUGGER_SUPPORT myRWPortBreakWidget = new CheckboxWidget(myTab, font, HBORDER + INDENT * 1, ypos + 1, "Break on reads from write ports"); + myRWPortBreakWidget->setToolTip("Cause reads from write ports to interrupt\n" + "emulation and enter debugger."); wid.push_back(myRWPortBreakWidget); ypos += lineHeight + VGAP; myWRPortBreakWidget = new CheckboxWidget(myTab, font, HBORDER + INDENT * 1, ypos + 1, "Break on writes to read ports"); + myWRPortBreakWidget->setToolTip("Cause writes to read ports to interrupt\n" + "emulation and enter debugger."); wid.push_back(myWRPortBreakWidget); ypos += lineHeight + VGAP; #endif @@ -186,12 +197,16 @@ void DeveloperDialog::addEmulationTab(const GUI::Font& font) // Thumb ARM emulation exception myThumbExceptionWidget = new CheckboxWidget(myTab, font, HBORDER + INDENT * 1, ypos + 1, "Fatal ARM emulation error throws exception"); + myThumbExceptionWidget->setToolTip("Cause Thumb ARM emulation to throw exceptions\n" + "on fatal errors and enter the debugger."); wid.push_back(myThumbExceptionWidget); ypos += lineHeight + VGAP; // AtariVox/SaveKey EEPROM access myEEPROMAccessWidget = new CheckboxWidget(myTab, font, HBORDER + INDENT * 1, ypos + 1, "Display AtariVox/SaveKey EEPROM R/W access"); + myEEPROMAccessWidget->setToolTip("Cause message display when AtariVox/\n" + "SaveKey EEPROM is read or written."); wid.push_back(myEEPROMAccessWidget); // Add items for tab 0 @@ -239,11 +254,15 @@ void DeveloperDialog::addTiaTab(const GUI::Font& font) VarList::push_back(items, "Custom", "custom"); myTIATypeWidget = new PopUpWidget(myTab, font, HBORDER + INDENT, ypos - 1, pwidth, lineHeight, items, "Chip type ", 0, kTIAType); + myTIATypeWidget->setToolTip("Select which TIA chip type to emulate.\n" + "Some types cause defined glitches."); wid.push_back(myTIATypeWidget); ypos += lineHeight + VGAP * 1; myInvPhaseLabel = new StaticTextWidget(myTab, font, HBORDER + INDENT * 2, ypos + 1, "Inverted HMOVE clock phase for"); + myInvPhaseLabel->setToolTip("Objects react different to too\n" + "early HM" + ELLIPSIS + " after HMOVE changes."); wid.push_back(myInvPhaseLabel); ypos += lineHeight + VGAP * 1; @@ -262,6 +281,7 @@ void DeveloperDialog::addTiaTab(const GUI::Font& font) myPlayfieldLabel = new StaticTextWidget(myTab, font, HBORDER + INDENT * 2, ypos + 1, "Delayed playfield"); + myPlayfieldLabel->setToolTip("Playfield reacts one color clock slower to updates."); wid.push_back(myPlayfieldLabel); ypos += lineHeight + VGAP * 1; @@ -275,6 +295,7 @@ void DeveloperDialog::addTiaTab(const GUI::Font& font) myBackgroundLabel = new StaticTextWidget(myTab, font, HBORDER + INDENT * 2, ypos + 1, "Delayed background"); + myBackgroundLabel->setToolTip("Background color reacts one color clock slower to updates."); wid.push_back(myBackgroundLabel); ypos += lineHeight + VGAP * 1; @@ -285,6 +306,7 @@ void DeveloperDialog::addTiaTab(const GUI::Font& font) ostringstream ss; ss << "Delayed VDEL" << ELLIPSIS << " swap for"; mySwapLabel = new StaticTextWidget(myTab, font, HBORDER + INDENT * 2, ypos + 1, ss.str()); + mySwapLabel->setToolTip("VDELed objects react one color clock slower to updates."); wid.push_back(mySwapLabel); ypos += lineHeight + VGAP * 1; @@ -332,12 +354,14 @@ void DeveloperDialog::addVideoTab(const GUI::Font& font) // TV jitter effect myTVJitterWidget = new CheckboxWidget(myTab, font, HBORDER + INDENT * 1, ypos + 1, "Jitter/roll effect", kTVJitter); + myTVJitterWidget->setToolTip("Enable to emulate TV loss of sync."); wid.push_back(myTVJitterWidget); myTVJitterRecWidget = new SliderWidget(myTab, font, myTVJitterWidget->getRight() + fontWidth * 3, ypos - 1, "Recovery ", 0, kTVJitterChanged); myTVJitterRecWidget->setMinValue(1); myTVJitterRecWidget->setMaxValue(20); myTVJitterRecWidget->setTickmarkIntervals(5); + myTVJitterRecWidget->setToolTip("Define speed of sync revovery."); wid.push_back(myTVJitterRecWidget); myTVJitterRecLabelWidget = new StaticTextWidget(myTab, font, myTVJitterRecWidget->getRight() + 4, @@ -347,6 +371,8 @@ void DeveloperDialog::addVideoTab(const GUI::Font& font) myColorLossWidget = new CheckboxWidget(myTab, font, HBORDER + INDENT * 1, ypos + 1, "PAL color-loss"); + myColorLossWidget->setToolTip("PAL games with odd scanline count\n" + "will be displayed without color."); wid.push_back(myColorLossWidget); ypos += lineHeight + VGAP; @@ -485,6 +511,7 @@ void DeveloperDialog::addTimeMachineTab(const GUI::Font& font) #endif myStateSizeWidget->setStepValue(20); myStateSizeWidget->setTickmarkIntervals(5); + myStateSizeWidget->setToolTip("Define the total Time Machine buffer size."); wid.push_back(myStateSizeWidget); ypos += lineHeight + VGAP; @@ -498,6 +525,9 @@ void DeveloperDialog::addTimeMachineTab(const GUI::Font& font) #endif myUncompressedWidget->setStepValue(20); myUncompressedWidget->setTickmarkIntervals(5); + myUncompressedWidget->setToolTip("Define the number of completely kept states.\n" + "States beyond this number will be slowly removed\n" + "to fit the requested horizon into the buffer."); wid.push_back(myUncompressedWidget); ypos += lineHeight + VGAP; @@ -507,6 +537,7 @@ void DeveloperDialog::addTimeMachineTab(const GUI::Font& font) int pwidth = font.getStringWidth("10 seconds"); myStateIntervalWidget = new PopUpWidget(myTab, font, xpos, ypos, pwidth, lineHeight, items, "Interval ", 0, kIntervalChanged); + myStateIntervalWidget->setToolTip("Define the interval between each saved state."); wid.push_back(myStateIntervalWidget); ypos += lineHeight + VGAP; @@ -515,6 +546,8 @@ void DeveloperDialog::addTimeMachineTab(const GUI::Font& font) VarList::push_back(items, HORIZONS[i], HOR_SETTINGS[i]); myStateHorizonWidget = new PopUpWidget(myTab, font, xpos, ypos, pwidth, lineHeight, items, "Horizon ~ ", 0, kHorizonChanged); + myStateHorizonWidget->setToolTip("Define how far the Time Machine\n" + "will allow moving back in time."); wid.push_back(myStateHorizonWidget); // Add message concerning usage @@ -598,6 +631,7 @@ void DeveloperDialog::addDebuggerTab(const GUI::Font& font) myGhostReadsTrapWidget = new CheckboxWidget(myTab, font, HBORDER, ypos + 1, "Trap on 'ghost' reads"); + myGhostReadsTrapWidget->setToolTip("Traps will consider CPU 'ghost' reads too."); wid.push_back(myGhostReadsTrapWidget); // Add message concerning usage From e4d9b2ecebc400494b978826c94239a1aa95e05c Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 20 Nov 2020 21:23:48 +0100 Subject: [PATCH 081/107] added wildcard support to launcher dialog filter --- Changes.txt | 12 ++++---- docs/index.html | 7 +++-- src/gui/LauncherDialog.cxx | 56 +++++++++++++++++++++++++++++++++++++- src/gui/LauncherDialog.hxx | 21 ++++++++++++++ src/gui/WhatsNewDialog.cxx | 1 + 5 files changed, 88 insertions(+), 9 deletions(-) diff --git a/Changes.txt b/Changes.txt index 4d5776bcb..6ec962851 100644 --- a/Changes.txt +++ b/Changes.txt @@ -14,15 +14,17 @@ 6.4 to 6.5 (December XX, 2020) - * Enhanced cut/copy/paste for text editing (TODO: PromptWidget) + * Enhanced cut/copy/paste for text editing. (TODO: PromptWidget) - * Added undo and redo to text editing (TODO: PromptWidget) + * Added undo and redo to text editing. (TODO: PromptWidget) - * Added static tooltips to some UI items + * Added wildcard support to launcher dialog filter. - * Added dynamic tooltips to most debugger items + * Added static tooltips to some UI items. - * Increased sample size for CDFJ+ + * Added dynamic tooltips to most debugger items. + + * Increased sample size for CDFJ+. -Have fun! diff --git a/docs/index.html b/docs/index.html index 0d2a8fd48..7e57b22e7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3695,9 +3695,10 @@ Typing characters here will show only those files that match that pattern. For example, typing 'Activision' will show only files that contain the word 'Activision' in their name. This is very useful for - quickly finding a group of related ROMs. Note that the search is not - case sensitive, so you don't need to worry about capital or lower-case - letters.

+ quickly finding a group of related ROMs.

+

+ Note that the search is not case sensitive, so you don't need to worry about + capital or lower-case letters. Also you can use '*' and '?' as wildcards.

ROM Launcher Context Menu

diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index cbab575bd..11fa0fc06 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -348,6 +348,60 @@ void LauncherDialog::updateUI() loadRomInfo(); } +size_t LauncherDialog::matchWithJoker(const string& str, const string& pattern) +{ + if(str.length() >= pattern.length()) + { + for(size_t pos = 0; pos < str.length() - pattern.length() + 1; ++pos) + { + bool found = true; + + for(size_t i = 0; found && i < pattern.length(); ++i) + if(pattern[i] != str[pos + i] && pattern[i] != '?') + found = false; + + if(found) + return pos; + } + } + return string::npos; +} + +bool LauncherDialog::matchWithWildcards(const string& str, const string& pattern) +{ + string in = str; + string pat = pattern; + size_t pos = string::npos; + + BSPF::toUpperCase(in); + BSPF::toUpperCase(pat); + + for(size_t i = 0; i < pat.length(); ++i) + if(pat[i] == '*') + { + pos = i; + break; + } + + if(pos != string::npos) + { + // '*' found, split pattern into left and right part, search recursively + const string leftPat = pat.substr(0, pos); + const string rightPat = pat.substr(pos + 1); + size_t posLeft = matchWithJoker(in, leftPat); + + if(posLeft != string::npos) + return matchWithWildcards(in.substr(pos + posLeft), rightPat); + else + return false; + } + else + { + // no further '*' found + return matchWithJoker(in, pat) != string::npos; + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void LauncherDialog::applyFiltering() { @@ -361,7 +415,7 @@ void LauncherDialog::applyFiltering() // Skip over files that don't match the pattern in the 'pattern' textbox if(myPattern && myPattern->getText() != "" && - !BSPF::containsIgnoreCase(node.getName(), myPattern->getText())) + !matchWithWildcards(node.getName(), myPattern->getText())) return false; } return true; diff --git a/src/gui/LauncherDialog.hxx b/src/gui/LauncherDialog.hxx index cc265928b..6fafab663 100644 --- a/src/gui/LauncherDialog.hxx +++ b/src/gui/LauncherDialog.hxx @@ -112,6 +112,27 @@ class LauncherDialog : public Dialog void loadConfig() override; void saveConfig() override; void updateUI(); + + /** + Search if string contains pattern including '?' as joker. + + @param str The searched string + @param pattern The pattern to search for + + @return Position of pattern in string. + */ + size_t matchWithJoker(const string& str, const string& pattern); + + /** + Search if string contains pattern including wildcard '*' + and '?' as joker, ignoring case. + + @param str The searched string + @param pattern The pattern to search for + + @return True if pattern was found. + */ + bool matchWithWildcards(const string& str, const string& pattern); void applyFiltering(); float getRomInfoZoom(int listHeight) const; diff --git a/src/gui/WhatsNewDialog.cxx b/src/gui/WhatsNewDialog.cxx index e94e432a2..5892bfcfd 100644 --- a/src/gui/WhatsNewDialog.cxx +++ b/src/gui/WhatsNewDialog.cxx @@ -48,6 +48,7 @@ WhatsNewDialog::WhatsNewDialog(OSystem& osystem, DialogContainer& parent, const #else add(ypos, "enhanced cut/copy/paste for text editing"); add(ypos, "added undo and redo to text editing"); + add(ypos, "added wildcard support to launcher dialog filter"); add(ypos, "added tooltips to many UI items"); add(ypos, "increased sample size for CDFJ+"); add(ypos, ELLIPSIS + " (for a complete list see 'docs/Changes.txt')"); From d330d6c716e9fc7a946606c96595bd91316c551a Mon Sep 17 00:00:00 2001 From: thrust26 Date: Fri, 20 Nov 2020 23:06:06 +0100 Subject: [PATCH 082/107] tooltips are disabled for R77 --- src/gui/Dialog.cxx | 4 +++- src/gui/DialogContainer.cxx | 8 ++------ src/gui/Widget.cxx | 6 ++++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 392f2b625..2ff6a4539 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -244,7 +244,7 @@ void Dialog::redraw(bool force) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Dialog::render() { - cerr << " render " << typeid(*this).name() << endl; + //cerr << " render " << typeid(*this).name() << endl; // Update dialog surface; also render any extra surfaces // Extra surfaces must be rendered afterwards, so they are drawn on top @@ -641,8 +641,10 @@ void Dialog::handleMouseMoved(int x, int y) if (w && (w->getFlags() & Widget::FLAG_TRACK_MOUSE)) w->handleMouseMoved(x - (w->getAbsX() - _x), y - (w->getAbsY() - _y)); +#ifndef RETRON77 // Update mouse coordinates for tooltips _toolTip->update(_mouseWidget, Common::Point(x, y)); +#endif } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 0a6730773..273b713c8 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -95,11 +95,7 @@ void DialogContainer::draw(bool full) if(myDialogStack.empty()) return; - cerr << "draw " << full << " " << typeid(*this).name() << endl; - - // Make the top dialog dirty if a full redraw is requested - //if(full) - // myDialogStack.top()->setDirty(); + //cerr << "draw " << full << " " << typeid(*this).name() << endl; // Draw and render all dirty dialogs myDialogStack.applyAll([&](Dialog*& d) { @@ -123,7 +119,7 @@ void DialogContainer::render() if(myDialogStack.empty()) return; - cerr << "full re-render " << typeid(*this).name() << endl; + //cerr << "full re-render " << typeid(*this).name() << endl; // Make sure we start in a clean state (with zero'ed buffers) if(!myOSystem.eventHandler().inTIAMode()) diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 1856c3f4b..e0b9a2389 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -75,8 +75,10 @@ void Widget::tick() { if(isEnabled()) { + #ifndef RETRON77 if(wantsToolTip()) dialog().tooltip().request(); + #endif // Recursively tick widget and all child dialogs and widgets Widget* w = _firstWidget; @@ -97,8 +99,8 @@ void Widget::draw() if(isDirty()) { - cerr << " *** draw widget " << typeid(*this).name() << " ***" << endl; - //cerr << "w"; + //cerr << " *** draw widget " << typeid(*this).name() << " ***" << endl; + cerr << "w"; FBSurface& s = _boss->dialog().surface(); int oldX = _x, oldY = _y; From 2141469ba7ce0e2827ba73cce32132dd63a1620c Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Fri, 20 Nov 2020 19:53:44 -0330 Subject: [PATCH 083/107] Fix typo in tooltip. --- src/gui/DeveloperDialog.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/DeveloperDialog.cxx b/src/gui/DeveloperDialog.cxx index e7d4f1e47..31443e1bf 100644 --- a/src/gui/DeveloperDialog.cxx +++ b/src/gui/DeveloperDialog.cxx @@ -361,7 +361,7 @@ void DeveloperDialog::addVideoTab(const GUI::Font& font) "Recovery ", 0, kTVJitterChanged); myTVJitterRecWidget->setMinValue(1); myTVJitterRecWidget->setMaxValue(20); myTVJitterRecWidget->setTickmarkIntervals(5); - myTVJitterRecWidget->setToolTip("Define speed of sync revovery."); + myTVJitterRecWidget->setToolTip("Define speed of sync recovery."); wid.push_back(myTVJitterRecWidget); myTVJitterRecLabelWidget = new StaticTextWidget(myTab, font, myTVJitterRecWidget->getRight() + 4, From 1636f1517cad1133096e0156711a3691fd215dbf Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 21 Nov 2020 14:38:32 +0100 Subject: [PATCH 084/107] fixed #732 --- src/emucore/FBSurface.cxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/emucore/FBSurface.cxx b/src/emucore/FBSurface.cxx index 6421dd171..bd7a3ad7b 100644 --- a/src/emucore/FBSurface.cxx +++ b/src/emucore/FBSurface.cxx @@ -299,6 +299,7 @@ void FBSurface::frameRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h, void FBSurface::splitString(const GUI::Font& font, const string& s, int w, string& left, string& right) const { +#ifdef GUI_SUPPORT uInt32 pos; int w2 = 0; bool split = false; @@ -329,6 +330,7 @@ void FBSurface::splitString(const GUI::Font& font, const string& s, int w, } left = s.substr(0, pos); right = s.substr(pos); +#endif } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 4c97ec89c9be1c7100c19ab359e5703e2ac89de9 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 21 Nov 2020 14:59:31 +0100 Subject: [PATCH 085/107] improved wildcard handling (addresses #154) --- src/gui/Dialog.cxx | 2 +- src/gui/LauncherDialog.cxx | 65 +++++++++++++++++++++++--------------- src/gui/LauncherDialog.hxx | 32 +++++++++++++------ 3 files changed, 63 insertions(+), 36 deletions(-) diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 2ff6a4539..9994bac45 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -264,7 +264,7 @@ void Dialog::render() if(!onTop) { - cerr << " shade " << typeid(*this).name() << endl; + //cerr << " shade " << typeid(*this).name() << endl; _shadeSurface->setDstRect(_surface->dstRect()); _shadeSurface->render(); diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 11fa0fc06..f2b80c7d2 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -352,54 +352,69 @@ size_t LauncherDialog::matchWithJoker(const string& str, const string& pattern) { if(str.length() >= pattern.length()) { - for(size_t pos = 0; pos < str.length() - pattern.length() + 1; ++pos) + // optimize a bit + if(pattern.find('?') != string::npos) { - bool found = true; + for(size_t pos = 0; pos < str.length() - pattern.length() + 1; ++pos) + { + bool found = true; - for(size_t i = 0; found && i < pattern.length(); ++i) - if(pattern[i] != str[pos + i] && pattern[i] != '?') - found = false; + for(size_t i = 0; found && i < pattern.length(); ++i) + if(pattern[i] != str[pos + i] && pattern[i] != '?') + found = false; - if(found) - return pos; + if(found) + return pos; + } } + else + return str.find(pattern); } return string::npos; } bool LauncherDialog::matchWithWildcards(const string& str, const string& pattern) { - string in = str; string pat = pattern; - size_t pos = string::npos; - BSPF::toUpperCase(in); - BSPF::toUpperCase(pat); + // remove leading and trailing '*' + size_t i = 0; + while(pat[i++] == '*'); + pat = pat.substr(i - 1); - for(size_t i = 0; i < pat.length(); ++i) - if(pat[i] == '*') - { - pos = i; - break; - } + i = pat.length(); + while(pat[--i] == '*'); + pat.erase(i + 1); + + // Search for first '*' + size_t pos = pat.find('*'); if(pos != string::npos) { // '*' found, split pattern into left and right part, search recursively const string leftPat = pat.substr(0, pos); const string rightPat = pat.substr(pos + 1); - size_t posLeft = matchWithJoker(in, leftPat); + size_t posLeft = matchWithJoker(str, leftPat); if(posLeft != string::npos) - return matchWithWildcards(in.substr(pos + posLeft), rightPat); + return matchWithWildcards(str.substr(pos + posLeft), rightPat); else return false; } - else - { - // no further '*' found - return matchWithJoker(in, pat) != string::npos; - } + // no further '*' found + return matchWithJoker(str, pat) != string::npos; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool LauncherDialog::matchWithWildcardsIgnoreCase(const string& str, const string& pattern) +{ + string in = str; + string pat = pattern; + + BSPF::toUpperCase(in); + BSPF::toUpperCase(pat); + + return matchWithWildcards(in, pat); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -415,7 +430,7 @@ void LauncherDialog::applyFiltering() // Skip over files that don't match the pattern in the 'pattern' textbox if(myPattern && myPattern->getText() != "" && - !matchWithWildcards(node.getName(), myPattern->getText())) + !matchWithWildcardsIgnoreCase(node.getName(), myPattern->getText())) return false; } return true; diff --git a/src/gui/LauncherDialog.hxx b/src/gui/LauncherDialog.hxx index 6fafab663..205fd090d 100644 --- a/src/gui/LauncherDialog.hxx +++ b/src/gui/LauncherDialog.hxx @@ -113,6 +113,28 @@ class LauncherDialog : public Dialog void saveConfig() override; void updateUI(); + /** + Search if string contains pattern including wildcard '*' + and '?' as joker, ignoring case. + + @param str The searched string + @param pattern The pattern to search for + + @return True if pattern was found. + */ + bool matchWithWildcardsIgnoreCase(const string& str, const string& pattern); + + /** + Search if string contains pattern including wildcard '*' + and '?' as joker. + + @param str The searched string + @param pattern The pattern to search for + + @return True if pattern was found. + */ + bool matchWithWildcards(const string& str, const string& pattern); + /** Search if string contains pattern including '?' as joker. @@ -123,16 +145,6 @@ class LauncherDialog : public Dialog */ size_t matchWithJoker(const string& str, const string& pattern); - /** - Search if string contains pattern including wildcard '*' - and '?' as joker, ignoring case. - - @param str The searched string - @param pattern The pattern to search for - - @return True if pattern was found. - */ - bool matchWithWildcards(const string& str, const string& pattern); void applyFiltering(); float getRomInfoZoom(int listHeight) const; From 1219fe0d2ca2bd8d0b7b820487f7a7fc03c7ce47 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sun, 22 Nov 2020 12:39:17 +0100 Subject: [PATCH 086/107] added subdirectory search to launcher enhanced ProgressDialog --- src/emucore/FSNode.cxx | 106 +++++++++++++++++++++++---- src/emucore/FSNode.hxx | 14 +++- src/gui/ContextMenu.hxx | 2 + src/gui/Dialog.cxx | 7 +- src/gui/Dialog.hxx | 2 + src/gui/DialogContainer.cxx | 2 +- src/gui/FileListWidget.cxx | 39 +++++++++- src/gui/FileListWidget.hxx | 18 ++++- src/gui/LauncherDialog.cxx | 138 +++++++++++++++++++++++++++++------- src/gui/LauncherDialog.hxx | 22 +++--- src/gui/ProgressDialog.cxx | 37 ++++++++-- src/gui/ProgressDialog.hxx | 8 ++- src/gui/ToolTip.cxx | 2 +- 13 files changed, 330 insertions(+), 67 deletions(-) diff --git a/src/emucore/FSNode.cxx b/src/emucore/FSNode.cxx index f2e5e477a..6be4078b0 100644 --- a/src/emucore/FSNode.cxx +++ b/src/emucore/FSNode.cxx @@ -76,9 +76,62 @@ bool FilesystemNode::exists() const return _realNode ? _realNode->exists() : false; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool FilesystemNode::getAllChildren(FSList& fslist, ListMode mode, + const NameFilter& filter, + bool includeParentDirectory) const +{ + if(getChildren(fslist, mode, filter, includeParentDirectory)) + { + // Sort only once at the end + #if defined(ZIP_SUPPORT) + // before sorting, replace single file ZIP archive names with contained file names + // because they are displayed using their contained file names + for(auto& i : fslist) + { + if(BSPF::endsWithIgnoreCase(i.getPath(), ".zip")) + { + FilesystemNodeZIP zipNode(i.getPath()); + + i.setName(zipNode.getName()); + } + } + #endif + + std::sort(fslist.begin(), fslist.end(), + [](const FilesystemNode& node1, const FilesystemNode& node2) + { + if(node1.isDirectory() != node2.isDirectory()) + return node1.isDirectory(); + else + return BSPF::compareIgnoreCase(node1.getName(), node2.getName()) < 0; + } + ); + + #if defined(ZIP_SUPPORT) + // After sorting replace zip files with zip nodes + for(auto& i : fslist) + { + if(BSPF::endsWithIgnoreCase(i.getPath(), ".zip")) + { + // Force ZIP c'tor to be called + AbstractFSNodePtr ptr = FilesystemNodeFactory::create(i.getPath(), + FilesystemNodeFactory::Type::ZIP); + FilesystemNode zipNode(ptr); + i = zipNode; + } + } + #endif + return true; + } + + return false; +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, const NameFilter& filter, + bool includeChildDirectories, bool includeParentDirectory) const { if (!_realNode || !_realNode->isDirectory()) @@ -90,12 +143,15 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, if (!_realNode->getChildren(tmp, mode)) return false; + // when incuding child directories, everything must be sorted once at the end + if(!includeChildDirectories) + { #if defined(ZIP_SUPPORT) // before sorting, replace single file ZIP archive names with contained file names // because they are displayed using their contained file names - for (auto& i : tmp) + for(auto& i : tmp) { - if (BSPF::endsWithIgnoreCase(i->getPath(), ".zip")) + if(BSPF::endsWithIgnoreCase(i->getPath(), ".zip")) { FilesystemNodeZIP node(i->getPath()); @@ -104,15 +160,16 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, } #endif - std::sort(tmp.begin(), tmp.end(), - [](const AbstractFSNodePtr& node1, const AbstractFSNodePtr& node2) + std::sort(tmp.begin(), tmp.end(), + [](const AbstractFSNodePtr& node1, const AbstractFSNodePtr& node2) { - if (node1->isDirectory() != node2->isDirectory()) + if(node1->isDirectory() != node2->isDirectory()) return node1->isDirectory(); else return BSPF::compareIgnoreCase(node1->getName(), node2->getName()) < 0; } - ); + ); + } // Add parent node, if it is valid to do so if (includeParentDirectory && hasParent()) @@ -130,21 +187,44 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, { // Force ZIP c'tor to be called AbstractFSNodePtr ptr = FilesystemNodeFactory::create(i->getPath(), - FilesystemNodeFactory::Type::ZIP); - FilesystemNode node(ptr); - if (filter(node)) - fslist.emplace_back(node); + FilesystemNodeFactory::Type::ZIP); + FilesystemNode zipNode(ptr); + + if(filter(zipNode)) + { + if(!includeChildDirectories) + fslist.emplace_back(zipNode); + else + { + // filter by zip node but add file node + FilesystemNode node(i); + fslist.emplace_back(node); + } + } } else #endif { // Make directories stand out - if (i->isDirectory()) + if(i->isDirectory()) i->setName(" [" + i->getName() + "]"); FilesystemNode node(i); - if (filter(node)) - fslist.emplace_back(node); + + if(includeChildDirectories) + { + if(i->isDirectory()) + node.getChildren(fslist, mode, filter, includeChildDirectories, false); + else + // do not add directories in this mode + if(filter(node)) + fslist.emplace_back(node); + } + else + { + if(filter(node)) + fslist.emplace_back(node); + } } } diff --git a/src/emucore/FSNode.hxx b/src/emucore/FSNode.hxx index cf76f9ac1..b7b93bd5d 100644 --- a/src/emucore/FSNode.hxx +++ b/src/emucore/FSNode.hxx @@ -114,6 +114,17 @@ class FilesystemNode */ bool exists() const; + /** + * Return a list of child nodes of this and all sub-directories. If called on a node + * that does not represent a directory, false is returned. + * + * @return true if successful, false otherwise (e.g. when the directory + * does not exist). + */ + bool getAllChildren(FSList& fslist, ListMode mode = ListMode::DirectoriesOnly, + const NameFilter& filter = [](const FilesystemNode&) { return true; }, + bool includeParentDirectory = true) const; + /** * Return a list of child nodes of this directory node. If called on a node * that does not represent a directory, false is returned. @@ -123,6 +134,7 @@ class FilesystemNode */ bool getChildren(FSList& fslist, ListMode mode = ListMode::DirectoriesOnly, const NameFilter& filter = [](const FilesystemNode&){ return true; }, + bool includeChildDirectories = false, bool includeParentDirectory = true) const; /** @@ -273,8 +285,8 @@ class FilesystemNode string getPathWithExt(const string& ext) const; private: - AbstractFSNodePtr _realNode; explicit FilesystemNode(const AbstractFSNodePtr& realNode); + AbstractFSNodePtr _realNode; void setPath(const string& path); }; diff --git a/src/gui/ContextMenu.hxx b/src/gui/ContextMenu.hxx index f1736e517..6807635ba 100644 --- a/src/gui/ContextMenu.hxx +++ b/src/gui/ContextMenu.hxx @@ -45,6 +45,8 @@ class ContextMenu : public Dialog, public CommandSender const VariantList& items, int cmd = 0, int width = 0); ~ContextMenu() override = default; + bool isShading() const override { return false; } + /** Set the parent widget's ID */ void setID(uInt32 id) { _id = id; } diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 9994bac45..50fcbd295 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -255,12 +255,11 @@ void Dialog::render() }); } - // Dialog is still on top if e.g a dialog without title is opened - // (e.g. ContextMenu) + // A dialog is still on top if a non-shading dialog (e.g. ContextMenu) + // is opended above it. bool onTop = parent().myDialogStack.top() == this || (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this - && !parent().myDialogStack.top()->hasTitle()); - //&& typeid(*parent().myDialogStack.top()) == typeid(ContextMenu)) + && !parent().myDialogStack.top()->isShading()); if(!onTop) { diff --git a/src/gui/Dialog.hxx b/src/gui/Dialog.hxx index 7935f1c95..a6fd7259b 100644 --- a/src/gui/Dialog.hxx +++ b/src/gui/Dialog.hxx @@ -94,6 +94,8 @@ class Dialog : public GuiObject void setTitle(const string& title); bool hasTitle() { return !_title.empty(); } + virtual bool isShading() const { return true; } + /** Determine the maximum width/height of a dialog based on the minimum allowable bounds, also taking into account the current window size. diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 273b713c8..16f32536a 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -171,7 +171,7 @@ void DialogContainer::removeDialog() { if(!myDialogStack.empty()) { - cerr << "remove dialog " << typeid(*myDialogStack.top()).name() << endl; + //cerr << "remove dialog " << typeid(*myDialogStack.top()).name() << endl; myDialogStack.pop(); // Inform the frame buffer that it has to render all surfaces diff --git a/src/gui/FileListWidget.cxx b/src/gui/FileListWidget.cxx index cb3b0f5c9..42d497d92 100644 --- a/src/gui/FileListWidget.cxx +++ b/src/gui/FileListWidget.cxx @@ -20,6 +20,7 @@ #include "ScrollBarWidget.hxx" #include "FileListWidget.hxx" #include "TimerManager.hxx" +#include "ProgressDialog.hxx" #include "bspf.hxx" @@ -72,22 +73,37 @@ void FileListWidget::setDirectory(const FilesystemNode& node, void FileListWidget::setLocation(const FilesystemNode& node, const string& select) { + progress().resetProgress(); + progress().open(); + _node = node; // Read in the data from the file system (start with an empty list) _fileList.clear(); - _fileList.reserve(512); - _node.getChildren(_fileList, _fsmode, _filter); + + if(_includeSubDirs) + { + // Actually this could become HUGE + _fileList.reserve(0x2000); + _node.getAllChildren(_fileList, _fsmode, _filter); + } + else + { + _fileList.reserve(0x200); + _node.getChildren(_fileList, _fsmode, _filter); + } // Now fill the list widget with the names from the file list StringList l; - for(const auto& file: _fileList) + for(const auto& file : _fileList) l.push_back(file.getName()); setList(l); setSelected(select); ListWidget::recalc(); + + progress().close(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -114,6 +130,21 @@ void FileListWidget::reload() } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +ProgressDialog& FileListWidget::progress() +{ + if(myProgressDialog == nullptr) + myProgressDialog = make_unique(this, _font, "", false); + + return *myProgressDialog; +} + +void FileListWidget::incProgress() +{ + if(_includeSubDirs) + progress().incProgress(); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool FileListWidget::handleText(char text) { @@ -202,3 +233,5 @@ void FileListWidget::handleCommand(CommandSender* sender, int cmd, int data, int // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - uInt64 FileListWidget::_QUICK_SELECT_DELAY = 300; + +unique_ptr FileListWidget::myProgressDialog{nullptr}; diff --git a/src/gui/FileListWidget.hxx b/src/gui/FileListWidget.hxx index 8b60fac44..b5ba5bf7e 100644 --- a/src/gui/FileListWidget.hxx +++ b/src/gui/FileListWidget.hxx @@ -19,6 +19,7 @@ #define FILE_LIST_WIDGET_HXX class CommandSender; +class ProgressDialog; #include "FSNode.hxx" #include "Stack.hxx" @@ -59,12 +60,16 @@ class FileListWidget : public StringListWidget _filter = filter; } + // When enabled, all subdirectories will be searched too. + void setIncludeSubDirs(bool enable) { _includeSubDirs = enable; } + /** Set initial directory, and optionally select the given item. - @param node The directory to display. If this is a file, its parent - will instead be used, and the file will be selected - @param select An optional entry to select (if applicable) + @param node The directory to display. If this is a file, its parent + will instead be used, and the file will be selected + @param select An optional entry to select (if applicable) + @param recursive Recursively list sub-directories too */ void setDirectory(const FilesystemNode& node, const string& select = EmptyString); @@ -84,6 +89,12 @@ class FileListWidget : public StringListWidget static void setQuickSelectDelay(uInt64 time) { _QUICK_SELECT_DELAY = time; } + ProgressDialog& progress(); + void incProgress(); + + protected: + static unique_ptr myProgressDialog; + private: /** Very similar to setDirectory(), but also updates the history */ void setLocation(const FilesystemNode& node, const string& select); @@ -99,6 +110,7 @@ class FileListWidget : public StringListWidget FilesystemNode::NameFilter _filter; FilesystemNode _node; FSList _fileList; + bool _includeSubDirs{false}; Common::FixedStack _history; uInt32 _selected{0}; diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index f2b80c7d2..c1e7d1328 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -15,6 +15,9 @@ // this file, and for a DISCLAIMER OF ALL WARRANTIES. //============================================================================ +// TODO: +// - abort current file list reload when typing + #include "bspf.hxx" #include "Bankswitch.hxx" #include "BrowserDialog.hxx" @@ -29,6 +32,7 @@ #include "GlobalPropsDialog.hxx" #include "StellaSettingsDialog.hxx" #include "WhatsNewDialog.hxx" +#include "ProgressDialog.hxx" #include "MessageBox.hxx" #include "ToolTip.hxx" #include "OSystem.hxx" @@ -73,25 +77,71 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, buttonHeight = myUseMinimalUI ? lineHeight - VGAP * 2: lineHeight * 1.25, buttonWidth = (_w - 2 * HBORDER - BUTTON_GAP * (4 - 1)); - int xpos = HBORDER, ypos = VBORDER, lwidth = 0, lwidth2 = 0; + int xpos = HBORDER, ypos = VBORDER; WidgetArray wid; - string lblRom = "Select a ROM from the list" + ELLIPSIS; + string lblSelect = "Select a ROM from the list" + ELLIPSIS; + string lblAllFiles = "Show all files"; const string& lblFilter = "Filter"; - const string& lblAllFiles = "Show all files"; - const string& lblFound = "XXXX items found"; + string lblSubDirs = "Incl. subdirectories"; + string lblFound = "12345 items found"; tooltip().setFont(font); - lwidth = font.getStringWidth(lblRom); - lwidth2 = font.getStringWidth(lblAllFiles) + CheckboxWidget::boxSize(font); - int lwidth3 = font.getStringWidth(lblFilter); - int lwidth4 = font.getStringWidth(lblFound); + int lwSelect = font.getStringWidth(lblSelect); + int cwAllFiles = font.getStringWidth(lblAllFiles) + CheckboxWidget::prefixSize(font); + int lwFilter = font.getStringWidth(lblFilter); + int cwSubDirs = font.getStringWidth(lblSubDirs) + CheckboxWidget::prefixSize(font); + int lwFound = font.getStringWidth(lblFound); + int wTotal = HBORDER * 2 + lwSelect + cwAllFiles + lwFilter + cwSubDirs + lwFound + + EditTextWidget::calcWidth(font, "123456") + LBL_GAP * 7; + bool noSelect = false; - if(w < HBORDER * 2 + lwidth + lwidth2 + lwidth3 + lwidth4 + fontWidth * 6 + LBL_GAP * 8) + if(w < wTotal) { // make sure there is space for at least 6 characters in the filter field - lblRom = "Select a ROM" + ELLIPSIS; - lwidth = font.getStringWidth(lblRom); + lblSelect = "Select a ROM" + ELLIPSIS; + int lwSelectShort = font.getStringWidth(lblSelect); + + wTotal -= lwSelect - lwSelectShort; + lwSelect = lwSelectShort; + } + if(w < wTotal) + { + // make sure there is space for at least 6 characters in the filter field + lblSubDirs = "Subdir."; + int cwSubDirsShort = font.getStringWidth(lblSubDirs) + CheckboxWidget::prefixSize(font); + + wTotal -= cwSubDirs - cwSubDirsShort; + cwSubDirs = cwSubDirsShort; + } + if(w < wTotal) + { + // make sure there is space for at least 6 characters in the filter field + lblAllFiles = "All files"; + int cwAllFilesShort = font.getStringWidth(lblAllFiles) + CheckboxWidget::prefixSize(font); + + wTotal -= cwAllFiles - cwAllFilesShort; + cwAllFiles = cwAllFilesShort; + } + if(w < wTotal) + { + // make sure there is space for at least 6 characters in the filter field + lblFound = "12345 found"; + int lwFoundShort = font.getStringWidth(lblFound); + + wTotal -= lwFound - lwFoundShort; + lwFound = lwFoundShort; + myShortCount = true; + } + if(w < wTotal) + { + // make sure there is space for at least 6 characters in the filter field + lblSelect = ""; + int lwSelectShort = font.getStringWidth(lblSelect); + + wTotal -= lwSelect - lwSelectShort; + lwSelect = lwSelectShort; + noSelect = true; } if(myUseMinimalUI) @@ -108,31 +158,51 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, } // Show the header - new StaticTextWidget(this, font, xpos, ypos, lblRom); + new StaticTextWidget(this, font, xpos, ypos, lblSelect); // Shop the files counter - xpos = _w - HBORDER - lwidth4; + xpos = _w - HBORDER - lwFound; myRomCount = new StaticTextWidget(this, font, xpos, ypos, - lwidth4, fontHeight, + lwFound, fontHeight, "", TextAlign::Right); // Add filter that can narrow the results shown in the listing // It has to fit between both labels if(!myUseMinimalUI && w >= 640) { - int fwidth = std::min(15 * fontWidth, xpos - lwidth3 - lwidth2 - lwidth - HBORDER - LBL_GAP * 8); + int fwFilter = std::min(EditTextWidget::calcWidth(font, "123456789012345"), + xpos - cwSubDirs - lwFilter - cwAllFiles + - lwSelect - HBORDER - LBL_GAP * (noSelect ? 5 : 7)); + + // Show the subdirectories checkbox + xpos -= cwSubDirs + LBL_GAP; + mySubDirs = new CheckboxWidget(this, font, xpos, ypos, lblSubDirs, kSubDirsCmd); + mySubDirs->setEnabled(false); + ostringstream tip; + tip << "Search files in subdirectories too.\n" + << "Filter must have at least " << MIN_SUBDIRS_CHARS << " chars."; + mySubDirs->setToolTip(tip.str()); + // Show the filter input field - xpos -= fwidth + LBL_GAP; - myPattern = new EditTextWidget(this, font, xpos, ypos - 2, fwidth, lineHeight, ""); - myPattern->setToolTip("Enter filter text to reduce file list."); + xpos -= fwFilter + LBL_GAP; + myPattern = new EditTextWidget(this, font, xpos, ypos - 2, fwFilter, lineHeight, ""); + myPattern->setToolTip("Enter filter text to reduce file list.\n" + "Use '*' and '?' as wildcards."); + // Show the "Filter" label - xpos -= lwidth3 + LBL_GAP; + xpos -= lwFilter + LBL_GAP; new StaticTextWidget(this, font, xpos, ypos, lblFilter); + // Show the checkbox for all files - xpos -= lwidth2 + LBL_GAP * 3; + if(noSelect) + xpos = HBORDER; + else + xpos -= cwAllFiles + LBL_GAP * 2; myAllFiles = new CheckboxWidget(this, font, xpos, ypos, lblAllFiles, kAllfilesCmd); myAllFiles->setToolTip("Uncheck to show ROM files only."); + wid.push_back(myAllFiles); wid.push_back(myPattern); + wid.push_back(mySubDirs); } // Add list with game titles @@ -167,11 +237,11 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, // Add textfield to show current directory xpos = HBORDER; - ypos += myList->getHeight() + VGAP * 2; - lwidth = font.getStringWidth("Path") + LBL_GAP; - myDirLabel = new StaticTextWidget(this, font, xpos, ypos+2, lwidth, fontHeight, + ypos += myList->getHeight() + VGAP; + lwSelect = font.getStringWidth("Path") + LBL_GAP; + myDirLabel = new StaticTextWidget(this, font, xpos, ypos+2, lwSelect, fontHeight, "Path", TextAlign::Left); - xpos += lwidth; + xpos += lwSelect; myDir = new EditTextWidget(this, font, xpos, ypos, _w - xpos - HBORDER, lineHeight, ""); myDir->setEditable(false, true); myDir->clearFlags(Widget::FLAG_RETAIN_FOCUS); @@ -236,9 +306,13 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, myGlobalProps = make_unique(this, myUseMinimalUI ? osystem.frameBuffer().launcherFont() : osystem.frameBuffer().font()); + // since we cannot know how many files there are, use are really high value here + myList->progress().setRange(0, 50000, 5); + myList->progress().setMessage(" Filtering files" + ELLIPSIS + " "); + // Do we show only ROMs or all files? bool onlyROMs = instance().settings().getBool("launcherroms"); - showOnlyROMs(onlyROMs); + if(myAllFiles) myAllFiles->setState(!onlyROMs); } @@ -341,7 +415,7 @@ void LauncherDialog::updateUI() // Indicate how many files were found ostringstream buf; - buf << (myList->getList().size() - 1) << " items found"; + buf << (myList->getList().size() - 1) << (myShortCount ? " found" : " items found"); myRomCount->setLabel(buf.str()); // Update ROM info UI item @@ -422,6 +496,7 @@ void LauncherDialog::applyFiltering() { myList->setNameFilter( [&](const FilesystemNode& node) { + myList->incProgress(); if(!node.isDirectory()) { // Do we want to show only ROMs or all files? @@ -664,6 +739,11 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd, reload(); break; + case kSubDirsCmd: + myList->setIncludeSubDirs(mySubDirs->getState()); + reload(); + break; + case kLoadROMCmd: case FileListWidget::ItemActivated: saveConfig(); @@ -689,9 +769,15 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd, break; case EditableWidget::kChangedCmd: + { + bool subAllowed = myPattern->getText().length() >= MIN_SUBDIRS_CHARS; + + mySubDirs->setEnabled(subAllowed); + myList->setIncludeSubDirs(mySubDirs->getState() && subAllowed); applyFiltering(); // pattern matching taken care of directly in this method reload(); break; + } case kQuitCmd: saveConfig(); diff --git a/src/gui/LauncherDialog.hxx b/src/gui/LauncherDialog.hxx index 205fd090d..44601d48d 100644 --- a/src/gui/LauncherDialog.hxx +++ b/src/gui/LauncherDialog.hxx @@ -100,6 +100,7 @@ class LauncherDialog : public Dialog static constexpr int MIN_ROMINFO_CHARS = 30; static constexpr int MIN_ROMINFO_ROWS = 7; // full lines static constexpr int MIN_ROMINFO_LINES = 4; // extra lines + static constexpr int MIN_SUBDIRS_CHARS = 3; // minimum filter chars for subdirectory search void setPosition() override { positionAt(0); } void handleKeyDown(StellaKey key, StellaMod mod, bool repeated) override; @@ -169,19 +170,22 @@ class LauncherDialog : public Dialog // automatically sized font for ROM info viewer unique_ptr myROMInfoFont; - ButtonWidget* myStartButton{nullptr}; - ButtonWidget* myPrevDirButton{nullptr}; - ButtonWidget* myOptionsButton{nullptr}; - ButtonWidget* myQuitButton{nullptr}; + CheckboxWidget* myAllFiles{nullptr}; + EditTextWidget* myPattern{nullptr}; + CheckboxWidget* mySubDirs{nullptr}; + StaticTextWidget* myRomCount{nullptr}; FileListWidget* myList{nullptr}; + StaticTextWidget* myDirLabel{nullptr}; EditTextWidget* myDir{nullptr}; - StaticTextWidget* myRomCount{nullptr}; - EditTextWidget* myPattern{nullptr}; - CheckboxWidget* myAllFiles{nullptr}; - RomInfoWidget* myRomInfoWidget{nullptr}; + ButtonWidget* myStartButton{nullptr}; + ButtonWidget* myPrevDirButton{nullptr}; + ButtonWidget* myOptionsButton{nullptr}; + ButtonWidget* myQuitButton{nullptr}; + + RomInfoWidget* myRomInfoWidget{nullptr}; std::unordered_map myMD5List; int mySelectedItem{0}; @@ -189,9 +193,11 @@ class LauncherDialog : public Dialog bool myShowOnlyROMs{false}; bool myUseMinimalUI{false}; bool myEventHandled{false}; + bool myShortCount{false}; enum { kAllfilesCmd = 'lalf', // show all files (or ROMs only) + kSubDirsCmd = 'lred', kPrevDirCmd = 'PRVD', kOptionsCmd = 'OPTI', kQuitCmd = 'QUIT' diff --git a/src/gui/ProgressDialog.cxx b/src/gui/ProgressDialog.cxx index 04bc2adae..026e0df82 100644 --- a/src/gui/ProgressDialog.cxx +++ b/src/gui/ProgressDialog.cxx @@ -26,8 +26,9 @@ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ProgressDialog::ProgressDialog(GuiObject* boss, const GUI::Font& font, - const string& message) - : Dialog(boss->instance(), boss->parent()) + const string& message, bool openDialog) + : Dialog(boss->instance(), boss->parent()), + myFont(font) { const int fontWidth = font.getMaxCharWidth(), fontHeight = font.getFontHeight(), @@ -52,13 +53,23 @@ ProgressDialog::ProgressDialog(GuiObject* boss, const GUI::Font& font, mySlider->setMinValue(1); mySlider->setMaxValue(100); - open(); + if(openDialog) + open(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ProgressDialog::setMessage(const string& message) { + const int fontWidth = myFont.getMaxCharWidth(), + HBORDER = fontWidth * 1.25; + const int lwidth = myFont.getStringWidth(message); + + // Recalculate real dimensions + _w = HBORDER * 2 + lwidth; + + myMessage->setWidth(lwidth); myMessage->setLabel(message); + mySlider->setWidth(lwidth); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -68,17 +79,25 @@ void ProgressDialog::setRange(int start, int finish, int step) myFinish = finish; myStep = int((step / 100.0) * (myFinish - myStart + 1)); - mySlider->setMinValue(myStart); + mySlider->setMinValue(myStart + myStep); mySlider->setMaxValue(myFinish); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ProgressDialog::resetProgress() +{ + myProgress = myStepProgress = 0; + mySlider->setValue(0); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ProgressDialog::setProgress(int progress) { // Only increase the progress bar if we have arrived at a new step - if(progress - mySlider->getValue() > myStep) + if(progress - myStepProgress >= myStep) { - mySlider->setValue(progress); + myStepProgress = progress; + mySlider->setValue(progress % (myFinish - myStart + 1)); // Since this dialog is usually called in a tight loop that doesn't // yield, we need to manually tell the framebuffer that a redraw is @@ -88,3 +107,9 @@ void ProgressDialog::setProgress(int progress) instance().frameBuffer().update(); } } + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ProgressDialog::incProgress() +{ + setProgress(++myProgress); +} diff --git a/src/gui/ProgressDialog.hxx b/src/gui/ProgressDialog.hxx index 0ec8731b5..1b36b9cef 100644 --- a/src/gui/ProgressDialog.hxx +++ b/src/gui/ProgressDialog.hxx @@ -23,23 +23,29 @@ class StaticTextWidget; class SliderWidget; #include "bspf.hxx" +#include "Dialog.hxx" class ProgressDialog : public Dialog { public: ProgressDialog(GuiObject* boss, const GUI::Font& font, - const string& message); + const string& message, bool openDialog = true); ~ProgressDialog() override = default; void setMessage(const string& message); void setRange(int begin, int end, int step); + void resetProgress(); void setProgress(int progress); + void incProgress(); private: + const GUI::Font& myFont; StaticTextWidget* myMessage{nullptr}; SliderWidget* mySlider{nullptr}; int myStart{0}, myFinish{0}, myStep{0}; + int myProgress{0}; + int myStepProgress{0}; private: // Following constructors and assignment operators not supported diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 6e6ef9fc9..a887e0096 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -179,5 +179,5 @@ void ToolTip::show(const string& tip) void ToolTip::render() { if(myTipShown) - mySurface->render(), cerr << " render tooltip" << endl; + mySurface->render(); // , cerr << " render tooltip" << endl; } From 38bea325c35857e1c32d0c030ba12b3fd53384b5 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sun, 22 Nov 2020 14:42:46 +0100 Subject: [PATCH 087/107] fixed considering "show all files" at startup fixed launcher focus issues after exiting ROMs --- src/gui/LauncherDialog.cxx | 4 ++-- src/gui/StringListWidget.cxx | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index c1e7d1328..03be5adf3 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -294,7 +294,7 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, if(myUseMinimalUI) // Highlight 'Rom Listing' mySelectedItem = 0; else - mySelectedItem = 2; + mySelectedItem = 3; addToFocusList(wid); @@ -312,7 +312,7 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, // Do we show only ROMs or all files? bool onlyROMs = instance().settings().getBool("launcherroms"); - + showOnlyROMs(onlyROMs); if(myAllFiles) myAllFiles->setState(!onlyROMs); } diff --git a/src/gui/StringListWidget.cxx b/src/gui/StringListWidget.cxx index 9173d6276..b94d1b5c4 100644 --- a/src/gui/StringListWidget.cxx +++ b/src/gui/StringListWidget.cxx @@ -82,13 +82,8 @@ string StringListWidget::getToolTip(const Common::Point& pos) const bool StringListWidget::changedToolTip(const Common::Point& oldPos, const Common::Point& newPos) const { - bool ch = getToolTipIndex(oldPos) != getToolTipIndex(newPos) + return getToolTipIndex(oldPos) != getToolTipIndex(newPos) && getToolTip(oldPos) != getToolTip(newPos); - - if(ch) - cerr << "changed" << endl; - - return ch; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 790ace5c56a37d9a3547a78705037b6736b66e48 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sun, 22 Nov 2020 21:58:49 +0100 Subject: [PATCH 088/107] fixed small font for launcher --- src/emucore/FrameBuffer.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index 91cabfe3e..f9ede53ab 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -198,7 +198,7 @@ void FrameBuffer::setupFonts() FontDesc FrameBuffer::getFontDesc(const string& name) const { if(name == "small") - return GUI::consoleBDesc; // 8x13 + return GUI::consoleDesc; // 8x13 else if(name == "low_medium") return GUI::consoleMediumBDesc; // 9x15 else if(name == "medium") From 7049bbfd663f1b6c05d85bab65819da48f65bdae Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sun, 22 Nov 2020 22:01:29 +0100 Subject: [PATCH 089/107] updated docs --- Changes.txt | 2 ++ docs/graphics/rominfo_1x_large.png | Bin 16977 -> 28882 bytes docs/graphics/rominfo_1x_small.png | Bin 12077 -> 19415 bytes docs/graphics/rominfo_2x_small.png | Bin 36268 -> 52980 bytes docs/index.html | 21 +++++++++------------ 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Changes.txt b/Changes.txt index 6ec962851..51992c85b 100644 --- a/Changes.txt +++ b/Changes.txt @@ -20,6 +20,8 @@ * Added wildcard support to launcher dialog filter. + * Added option to search subdirectories in launcher. + * Added static tooltips to some UI items. * Added dynamic tooltips to most debugger items. diff --git a/docs/graphics/rominfo_1x_large.png b/docs/graphics/rominfo_1x_large.png index e5b08d857106049c1e5496a519aa3730529dcb4e..99fee27d28ed56513c134a6d60d39ae67299d18f 100644 GIT binary patch literal 28882 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�KP>XO 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 literal 16977 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 diff --git a/docs/graphics/rominfo_2x_small.png b/docs/graphics/rominfo_2x_small.png index da610bba01d5976ca3b3f9869ecc249a5dd610b1..4aa4d6ccab2a868d54a02d46d8b82a2c21274d0d 100644 GIT binary patch literal 52980 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∓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

x4ir2PL1;&G{CcAIYNS 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 diff --git a/docs/index.html b/docs/index.html index 7e57b22e7..d9080cae9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3689,16 +3689,19 @@

ROM Info Viewer width at 50% , UI sized 1280x900, large launcher font:

-

The text box in the upper right corner can be used to narrow down the - results in the ROM listing. When this box is empty, all files are shown - (subject to the restrictions from the filtering option, explained below). +

The 'Show all files' checkbox allows displaying files which do not + have a valid ROM extension. The 'Filter' text box can be used to narrow down + the results in the ROM listing. When this box is empty, all files are shown. Typing characters here will show only those files that match that pattern. For example, typing 'Activision' will show only files that contain the word 'Activision' in their name. This is very useful for quickly finding a group of related ROMs.

-

- Note that the search is not case sensitive, so you don't need to worry about - capital or lower-case letters. Also you can use '*' and '?' as wildcards.

+

Note that the search is not case sensitive, so you don't need to worry about + capital or lower-case letters. Also you can use '*' and '?' as wildcards. E.g. + for '(198?)*atari' only games from the 1980s made by Atari will be listed.

+

When there are least three characters in the filter, the checkbox 'Incl. + subdirectories' gets enabled. When checked, Stella will search files in all + subdirectories too.

ROM Launcher Context Menu

@@ -3736,12 +3739,6 @@ -
  • Show only ROM files: Selecting this reloads the current listing, - showing only files that have a valid ROM extension.
  • - -
  • Show all files: Selecting this reloads the current listing, - showing all files (with no restriction on file name).
  • -
  • Reload listing: Selecting this performs a reload of the current listing. It is an alternative to pressing the 'Control + r' key combo.
  • From 6e4052763b63a8eac7c3ef95d6126653916a75b8 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Mon, 23 Nov 2020 09:08:26 +0100 Subject: [PATCH 090/107] added launcher reload delay while typing filter --- src/gui/FileListWidget.hxx | 1 + src/gui/LauncherDialog.cxx | 24 ++++++++++++++++++++++-- src/gui/LauncherDialog.hxx | 4 ++++ src/gui/WhatsNewDialog.cxx | 1 + 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/gui/FileListWidget.hxx b/src/gui/FileListWidget.hxx index b5ba5bf7e..98c149223 100644 --- a/src/gui/FileListWidget.hxx +++ b/src/gui/FileListWidget.hxx @@ -88,6 +88,7 @@ class FileListWidget : public StringListWidget const FilesystemNode& currentDir() const { return _node; } static void setQuickSelectDelay(uInt64 time) { _QUICK_SELECT_DELAY = time; } + uInt64 getQuickSelectDelay() { return _QUICK_SELECT_DELAY; } ProgressDialog& progress(); void incProgress(); diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 03be5adf3..74f08328a 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -35,6 +35,7 @@ #include "ProgressDialog.hxx" #include "MessageBox.hxx" #include "ToolTip.hxx" +#include "TimerManager.hxx" #include "OSystem.hxx" #include "FrameBuffer.hxx" #include "FBSurface.hxx" @@ -358,6 +359,16 @@ void LauncherDialog::reload() { myMD5List.clear(); myList->reload(); + myPendingReload = false; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void LauncherDialog::tick() +{ + if(myPendingReload && myReloadTime < TimerManager::getTicks() / 1000) + reload(); + + Dialog::tick(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -771,11 +782,20 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd, case EditableWidget::kChangedCmd: { bool subAllowed = myPattern->getText().length() >= MIN_SUBDIRS_CHARS; + bool subDirs = subAllowed && mySubDirs->getState(); mySubDirs->setEnabled(subAllowed); - myList->setIncludeSubDirs(mySubDirs->getState() && subAllowed); + myList->setIncludeSubDirs(subDirs); applyFiltering(); // pattern matching taken care of directly in this method - reload(); + + if(subDirs) + { + // delay (potentially slow) subdirectories reloads until user stops typing + myReloadTime = TimerManager::getTicks() / 1000 + myList->getQuickSelectDelay(); + myPendingReload = true; + } + else + reload(); break; } diff --git a/src/gui/LauncherDialog.hxx b/src/gui/LauncherDialog.hxx index 44601d48d..a415f1403 100644 --- a/src/gui/LauncherDialog.hxx +++ b/src/gui/LauncherDialog.hxx @@ -95,6 +95,8 @@ class LauncherDialog : public Dialog */ void reload(); + void tick() override; + private: static constexpr int MIN_LAUNCHER_CHARS = 24; static constexpr int MIN_ROMINFO_CHARS = 30; @@ -194,6 +196,8 @@ class LauncherDialog : public Dialog bool myUseMinimalUI{false}; bool myEventHandled{false}; bool myShortCount{false}; + bool myPendingReload{false}; + uInt64 myReloadTime{0}; enum { kAllfilesCmd = 'lalf', // show all files (or ROMs only) diff --git a/src/gui/WhatsNewDialog.cxx b/src/gui/WhatsNewDialog.cxx index 5892bfcfd..7c26ca186 100644 --- a/src/gui/WhatsNewDialog.cxx +++ b/src/gui/WhatsNewDialog.cxx @@ -49,6 +49,7 @@ WhatsNewDialog::WhatsNewDialog(OSystem& osystem, DialogContainer& parent, const add(ypos, "enhanced cut/copy/paste for text editing"); add(ypos, "added undo and redo to text editing"); add(ypos, "added wildcard support to launcher dialog filter"); + add(ypos, "added option to search subdirectories in launcher"); add(ypos, "added tooltips to many UI items"); add(ypos, "increased sample size for CDFJ+"); add(ypos, ELLIPSIS + " (for a complete list see 'docs/Changes.txt')"); From b569444854ce527abf20c62ca45af06227901257 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Mon, 23 Nov 2020 22:02:52 +0100 Subject: [PATCH 091/107] added cancel option (button, enter, ESC) to ProgressDialog adapted all ProgressDialog using actions to allow canceling --- docs/index.html | 36 +++++++++++------- src/debugger/DebuggerParser.cxx | 43 +++++++++++++-------- src/emucore/FSNode.cxx | 18 ++++++--- src/emucore/FSNode.hxx | 7 +++- src/gui/FileListWidget.cxx | 24 +++++++----- src/gui/LauncherDialog.cxx | 11 ++---- src/gui/LauncherDialog.hxx | 1 - src/gui/ProgressDialog.cxx | 67 +++++++++++++++++++++++++-------- src/gui/ProgressDialog.hxx | 8 +++- src/gui/RomAuditDialog.cxx | 12 ++++-- src/gui/Widget.cxx | 30 +++++++++++++++ src/gui/Widget.hxx | 4 ++ 12 files changed, 187 insertions(+), 74 deletions(-) diff --git a/docs/index.html b/docs/index.html index d9080cae9..f1a9ecb25 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3689,19 +3689,29 @@

    ROM Info Viewer width at 50% , UI sized 1280x900, large launcher font:

    -

    The 'Show all files' checkbox allows displaying files which do not - have a valid ROM extension. The 'Filter' text box can be used to narrow down - the results in the ROM listing. When this box is empty, all files are shown. - Typing characters here will show only those files that match that - pattern. For example, typing 'Activision' will show only files that - contain the word 'Activision' in their name. This is very useful for - quickly finding a group of related ROMs.

    -

    Note that the search is not case sensitive, so you don't need to worry about - capital or lower-case letters. Also you can use '*' and '?' as wildcards. E.g. - for '(198?)*atari' only games from the 1980s made by Atari will be listed.

    -

    When there are least three characters in the filter, the checkbox 'Incl. - subdirectories' gets enabled. When checked, Stella will search files in all - subdirectories too.

    +

    The dialog items at the top can be used to define the listed files:

    + +
      +
    • + The 'Show all files' checkbox allows displaying files which do not + have a valid ROM extension. +
    • + The 'Filter' text box can be used to narrow down the results in the + ROM listing. When this box is empty, all files are shown. Typing + characters here will show only those files that match that + pattern. For example, typing 'Activision' will show only files that + contain the word 'Activision' in their name. This is very useful for + quickly finding a group of related ROMs.
      + Note that the search is not case sensitive, so you don't need to worry + about capital or lower-case letters. You also can use '*' and '?' as + wildcards. E.g. for '(198?)*atari' only ROMs from the 1980s made by + Atari will be listed. +
    • + If 'Incl. subdirectories' is checked, Stella will list matching files + from all subdirectories too. +
    • +
    +

    ROM Launcher Context Menu

    diff --git a/src/debugger/DebuggerParser.cxx b/src/debugger/DebuggerParser.cxx index 73f09268f..cd631bc7e 100644 --- a/src/debugger/DebuggerParser.cxx +++ b/src/debugger/DebuggerParser.cxx @@ -1747,9 +1747,13 @@ void DebuggerParser::executeRunTo() // Create a progress dialog box to show the progress searching through the // disassembly, since this may be a time-consuming operation ostringstream buf; - buf << "RunTo searching through " << max_iterations << " disassembled instructions"; - ProgressDialog progress(debugger.baseDialog(), debugger.lfont(), buf.str()); + ProgressDialog progress(debugger.baseDialog(), debugger.lfont()); + + buf << "RunTo searching through " << max_iterations << " disassembled instructions" + << progress.ELLIPSIS; + progress.setMessage(buf.str()); progress.setRange(0, max_iterations, 5); + progress.open(); bool done = false; do { @@ -1763,8 +1767,8 @@ void DebuggerParser::executeRunTo() done = (BSPF::findIgnoreCase(next, argStrings[0]) != string::npos); } // Update the progress bar - progress.setProgress(count); - } while(!done && ++count < max_iterations); + progress.incProgress(); + } while(!done && ++count < max_iterations && !progress.isCancelled()); progress.close(); @@ -1789,13 +1793,15 @@ void DebuggerParser::executeRunToPc() uInt32 count = 0; bool done = false; - constexpr uInt32 max_iterations = 1000000; // Create a progress dialog box to show the progress searching through the // disassembly, since this may be a time-consuming operation ostringstream buf; - buf << "RunTo PC searching through " << max_iterations << " instructions"; - ProgressDialog progress(debugger.baseDialog(), debugger.lfont(), buf.str()); - progress.setRange(0, max_iterations, 5); + ProgressDialog progress(debugger.baseDialog(), debugger.lfont()); + + buf << " RunTo PC running" << progress.ELLIPSIS << " "; + progress.setMessage(buf.str()); + progress.setRange(0, 100000, 5); + progress.open(); do { debugger.step(false); @@ -1803,8 +1809,9 @@ void DebuggerParser::executeRunToPc() // Update romlist to point to current PC int pcline = cartdbg.addressToLine(debugger.cpuDebug().pc()); done = (pcline >= 0) && (list[pcline].address == args[0]); - progress.setProgress(count); - } while(!done && ++count < max_iterations/*list.size()*/); + progress.incProgress(); + ++count; + } while(!done && !progress.isCancelled()); progress.close(); if(done) @@ -1953,20 +1960,24 @@ void DebuggerParser::executeStepwhile() Expression* expr = YaccParser::getResult(); int ncycles = 0; uInt32 count = 0; - constexpr uInt32 max_iterations = 1000000; // Create a progress dialog box to show the progress searching through the // disassembly, since this may be a time-consuming operation ostringstream buf; - buf << "stepwhile running through " << max_iterations << " disassembled instructions"; - ProgressDialog progress(debugger.baseDialog(), debugger.lfont(), buf.str()); - progress.setRange(0, max_iterations, 5); + ProgressDialog progress(debugger.baseDialog(), debugger.lfont()); + + buf << "stepwhile running through disassembled instructions" + << progress.ELLIPSIS; + progress.setMessage(buf.str()); + progress.setRange(0, 100000, 5); + progress.open(); do { ncycles += debugger.step(false); - progress.setProgress(count); - } while (expr->evaluate() && ++count < max_iterations); + progress.incProgress(); + ++count; + } while (expr->evaluate() && !progress.isCancelled()); progress.close(); commandResult << "executed " << ncycles << " cycles"; diff --git a/src/emucore/FSNode.cxx b/src/emucore/FSNode.cxx index 6be4078b0..6ff0b6482 100644 --- a/src/emucore/FSNode.cxx +++ b/src/emucore/FSNode.cxx @@ -79,9 +79,10 @@ bool FilesystemNode::exists() const // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool FilesystemNode::getAllChildren(FSList& fslist, ListMode mode, const NameFilter& filter, - bool includeParentDirectory) const + bool includeParentDirectory, + const CancelCheck& isCancelled) const { - if(getChildren(fslist, mode, filter, includeParentDirectory)) + if(getChildren(fslist, mode, filter, includeParentDirectory, true, isCancelled)) { // Sort only once at the end #if defined(ZIP_SUPPORT) @@ -124,7 +125,6 @@ bool FilesystemNode::getAllChildren(FSList& fslist, ListMode mode, #endif return true; } - return false; } @@ -132,7 +132,8 @@ bool FilesystemNode::getAllChildren(FSList& fslist, ListMode mode, bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, const NameFilter& filter, bool includeChildDirectories, - bool includeParentDirectory) const + bool includeParentDirectory, + const CancelCheck& isCancelled) const { if (!_realNode || !_realNode->isDirectory()) return false; @@ -146,6 +147,9 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, // when incuding child directories, everything must be sorted once at the end if(!includeChildDirectories) { + if(isCancelled()) + return false; + #if defined(ZIP_SUPPORT) // before sorting, replace single file ZIP archive names with contained file names // because they are displayed using their contained file names @@ -182,6 +186,9 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, // And now add the rest of the entries for (const auto& i: tmp) { + if(isCancelled()) + return false; + #if defined(ZIP_SUPPORT) if (BSPF::endsWithIgnoreCase(i->getPath(), ".zip")) { @@ -214,7 +221,7 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, if(includeChildDirectories) { if(i->isDirectory()) - node.getChildren(fslist, mode, filter, includeChildDirectories, false); + node.getChildren(fslist, mode, filter, includeChildDirectories, false, isCancelled); else // do not add directories in this mode if(filter(node)) @@ -227,7 +234,6 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, } } } - return true; } diff --git a/src/emucore/FSNode.hxx b/src/emucore/FSNode.hxx index b7b93bd5d..05378d185 100644 --- a/src/emucore/FSNode.hxx +++ b/src/emucore/FSNode.hxx @@ -57,6 +57,7 @@ class FilesystemNode /** Function used to filter the file listing. Returns true if the filename should be included, else false.*/ using NameFilter = std::function; + using CancelCheck = std::function const; /** * Create a new pathless FilesystemNode. Since there's no path associated @@ -123,7 +124,8 @@ class FilesystemNode */ bool getAllChildren(FSList& fslist, ListMode mode = ListMode::DirectoriesOnly, const NameFilter& filter = [](const FilesystemNode&) { return true; }, - bool includeParentDirectory = true) const; + bool includeParentDirectory = true, + const CancelCheck& isCancelled = []() { return false; }) const; /** * Return a list of child nodes of this directory node. If called on a node @@ -135,7 +137,8 @@ class FilesystemNode bool getChildren(FSList& fslist, ListMode mode = ListMode::DirectoriesOnly, const NameFilter& filter = [](const FilesystemNode&){ return true; }, bool includeChildDirectories = false, - bool includeParentDirectory = true) const; + bool includeParentDirectory = true, + const CancelCheck& isCancelled = []() { return false; }) const; /** * Set/get a string representation of the name of the file. This is can be diff --git a/src/gui/FileListWidget.cxx b/src/gui/FileListWidget.cxx index 42d497d92..be68d632f 100644 --- a/src/gui/FileListWidget.cxx +++ b/src/gui/FileListWidget.cxx @@ -75,6 +75,9 @@ void FileListWidget::setLocation(const FilesystemNode& node, { progress().resetProgress(); progress().open(); + class FilesystemNode::CancelCheck isCancelled = []() { + return myProgressDialog->isCancelled(); + }; _node = node; @@ -85,21 +88,24 @@ void FileListWidget::setLocation(const FilesystemNode& node, { // Actually this could become HUGE _fileList.reserve(0x2000); - _node.getAllChildren(_fileList, _fsmode, _filter); + _node.getAllChildren(_fileList, _fsmode, _filter, true, isCancelled); } else { _fileList.reserve(0x200); - _node.getChildren(_fileList, _fsmode, _filter); + _node.getChildren(_fileList, _fsmode, _filter, false, true, isCancelled); } - // Now fill the list widget with the names from the file list - StringList l; - for(const auto& file : _fileList) - l.push_back(file.getName()); + if(!isCancelled()) + { + // Now fill the list widget with the names from the file list + StringList l; + for(const auto& file : _fileList) + l.push_back(file.getName()); - setList(l); - setSelected(select); + setList(l); + setSelected(select); + } ListWidget::recalc(); @@ -134,7 +140,7 @@ void FileListWidget::reload() ProgressDialog& FileListWidget::progress() { if(myProgressDialog == nullptr) - myProgressDialog = make_unique(this, _font, "", false); + myProgressDialog = make_unique(this, _font, ""); return *myProgressDialog; } diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 74f08328a..87d0938b4 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -177,10 +177,8 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, // Show the subdirectories checkbox xpos -= cwSubDirs + LBL_GAP; mySubDirs = new CheckboxWidget(this, font, xpos, ypos, lblSubDirs, kSubDirsCmd); - mySubDirs->setEnabled(false); ostringstream tip; - tip << "Search files in subdirectories too.\n" - << "Filter must have at least " << MIN_SUBDIRS_CHARS << " chars."; + tip << "Search files in subdirectories too."; mySubDirs->setToolTip(tip.str()); // Show the filter input field @@ -780,15 +778,14 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd, break; case EditableWidget::kChangedCmd: + case EditableWidget::kAcceptCmd: { - bool subAllowed = myPattern->getText().length() >= MIN_SUBDIRS_CHARS; - bool subDirs = subAllowed && mySubDirs->getState(); + bool subDirs = mySubDirs->getState(); - mySubDirs->setEnabled(subAllowed); myList->setIncludeSubDirs(subDirs); applyFiltering(); // pattern matching taken care of directly in this method - if(subDirs) + if(subDirs && cmd == EditableWidget::kChangedCmd) { // delay (potentially slow) subdirectories reloads until user stops typing myReloadTime = TimerManager::getTicks() / 1000 + myList->getQuickSelectDelay(); diff --git a/src/gui/LauncherDialog.hxx b/src/gui/LauncherDialog.hxx index a415f1403..a5acc262f 100644 --- a/src/gui/LauncherDialog.hxx +++ b/src/gui/LauncherDialog.hxx @@ -102,7 +102,6 @@ class LauncherDialog : public Dialog static constexpr int MIN_ROMINFO_CHARS = 30; static constexpr int MIN_ROMINFO_ROWS = 7; // full lines static constexpr int MIN_ROMINFO_LINES = 4; // extra lines - static constexpr int MIN_SUBDIRS_CHARS = 3; // minimum filter chars for subdirectory search void setPosition() override { positionAt(0); } void handleKeyDown(StellaKey key, StellaMod mod, bool repeated) override; diff --git a/src/gui/ProgressDialog.cxx b/src/gui/ProgressDialog.cxx index 026e0df82..e36cf6008 100644 --- a/src/gui/ProgressDialog.cxx +++ b/src/gui/ProgressDialog.cxx @@ -18,6 +18,8 @@ #include "bspf.hxx" #include "OSystem.hxx" #include "FrameBuffer.hxx" +#include "EventHandler.hxx" +#include "TimerManager.hxx" #include "Widget.hxx" #include "Dialog.hxx" #include "Font.hxx" @@ -26,7 +28,7 @@ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ProgressDialog::ProgressDialog(GuiObject* boss, const GUI::Font& font, - const string& message, bool openDialog) + const string& message) : Dialog(boss->instance(), boss->parent()), myFont(font) { @@ -35,41 +37,56 @@ ProgressDialog::ProgressDialog(GuiObject* boss, const GUI::Font& font, lineHeight = font.getLineHeight(), VBORDER = fontHeight / 2, HBORDER = fontWidth * 1.25, - VGAP = fontHeight / 4; - int xpos, ypos, lwidth; + VGAP = fontHeight / 4, + buttonHeight = font.getLineHeight() * 1.25, + BTN_BORDER = fontWidth * 2.5, + buttonWidth = font.getStringWidth("Cancel") + BTN_BORDER, + lwidth = font.getStringWidth(message); + + int xpos, ypos; + WidgetArray wid; // Calculate real dimensions - lwidth = font.getStringWidth(message); - _w = HBORDER * 2 + lwidth; - _h = VBORDER * 2 + lineHeight * 2 + VGAP * 2; + _w = HBORDER * 2 + std::max(lwidth, buttonWidth); + _h = VBORDER * 2 + lineHeight * 2 + buttonHeight + VGAP * 6; xpos = HBORDER; ypos = VBORDER; myMessage = new StaticTextWidget(this, font, xpos, ypos, lwidth, fontHeight, message, TextAlign::Center); myMessage->setTextColor(kTextColorEm); - xpos = HBORDER; ypos += lineHeight + VGAP * 2; - mySlider = new SliderWidget(this, font, xpos, ypos, lwidth, lineHeight, "", 0, 0); + ypos += lineHeight + VGAP * 2; + mySlider = new SliderWidget(this, font, xpos, ypos, lwidth, lineHeight, + "", 0, 0); mySlider->setMinValue(1); mySlider->setMaxValue(100); - if(openDialog) - open(); + ypos += lineHeight + VGAP * 4; + ButtonWidget* b = new ButtonWidget(this, font, (_w - buttonWidth) / 2, ypos, + buttonWidth, buttonHeight, "Cancel", + Event::UICancel); + wid.push_back(b); + addCancelWidget(b); + addToFocusList(wid); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ProgressDialog::setMessage(const string& message) { const int fontWidth = myFont.getMaxCharWidth(), - HBORDER = fontWidth * 1.25; - const int lwidth = myFont.getStringWidth(message); + HBORDER = fontWidth * 1.25, + lwidth = myFont.getStringWidth(message), + BTN_BORDER = fontWidth * 2.5, + buttonWidth = myFont.getStringWidth("Cancel") + BTN_BORDER; // Recalculate real dimensions - _w = HBORDER * 2 + lwidth; + _w = HBORDER * 2 + std::max(lwidth, buttonWidth); myMessage->setWidth(lwidth); myMessage->setLabel(message); mySlider->setWidth(lwidth); + + _cancelWidget->setPos((_w - buttonWidth) / 2, _cancelWidget->getTop()); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -88,6 +105,7 @@ void ProgressDialog::resetProgress() { myProgress = myStepProgress = 0; mySlider->setValue(0); + myIsCancelled = false; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -100,11 +118,13 @@ void ProgressDialog::setProgress(int progress) mySlider->setValue(progress % (myFinish - myStart + 1)); // Since this dialog is usually called in a tight loop that doesn't - // yield, we need to manually tell the framebuffer that a redraw is - // necessary + // yield, we need to manually: + // - tell the framebuffer that a redraw is necessary + // - poll the events // This isn't really an ideal solution, since all redrawing and // event handling is suspended until the dialog is closed instance().frameBuffer().update(); + instance().eventHandler().poll(TimerManager::getTicks()); } } @@ -113,3 +133,20 @@ void ProgressDialog::incProgress() { setProgress(++myProgress); } + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ProgressDialog::handleCommand(CommandSender* sender, int cmd, + int data, int id) +{ + switch(cmd) + { + case Event::UICancel: + myIsCancelled = true; + break; + + default: + Dialog::handleCommand(sender, cmd, data, 0); + break; + } +} + diff --git a/src/gui/ProgressDialog.hxx b/src/gui/ProgressDialog.hxx index 1b36b9cef..a62fb8cbf 100644 --- a/src/gui/ProgressDialog.hxx +++ b/src/gui/ProgressDialog.hxx @@ -21,6 +21,7 @@ class GuiObject; class StaticTextWidget; class SliderWidget; +class ButtonWidget; #include "bspf.hxx" #include "Dialog.hxx" @@ -29,7 +30,7 @@ class ProgressDialog : public Dialog { public: ProgressDialog(GuiObject* boss, const GUI::Font& font, - const string& message, bool openDialog = true); + const string& message = ""); ~ProgressDialog() override = default; void setMessage(const string& message); @@ -37,6 +38,7 @@ class ProgressDialog : public Dialog void resetProgress(); void setProgress(int progress); void incProgress(); + bool isCancelled() const { return myIsCancelled; } private: const GUI::Font& myFont; @@ -46,6 +48,10 @@ class ProgressDialog : public Dialog int myStart{0}, myFinish{0}, myStep{0}; int myProgress{0}; int myStepProgress{0}; + bool myIsCancelled{false}; + + private: + void handleCommand(CommandSender* sender, int cmd, int data, int id) override; private: // Following constructors and assignment operators not supported diff --git a/src/gui/RomAuditDialog.cxx b/src/gui/RomAuditDialog.cxx index 09c264025..a95e4fce5 100644 --- a/src/gui/RomAuditDialog.cxx +++ b/src/gui/RomAuditDialog.cxx @@ -119,13 +119,17 @@ void RomAuditDialog::auditRoms() // Create a progress dialog box to show the progress of processing // the ROMs, since this is usually a time-consuming operation - ProgressDialog progress(this, instance().frameBuffer().font(), - "Auditing ROM files ..."); + ostringstream buf; + ProgressDialog progress(this, instance().frameBuffer().font()); + + buf << "Auditing ROM files" << ELLIPSIS; + progress.setMessage(buf.str()); progress.setRange(0, int(files.size()) - 1, 5); + progress.open(); Properties props; uInt32 renamed = 0, notfound = 0; - for(uInt32 idx = 0; idx < files.size(); ++idx) + for(uInt32 idx = 0; idx < files.size() && !progress.isCancelled(); ++idx) { string extension; if(files[idx].isFile() && @@ -156,7 +160,7 @@ void RomAuditDialog::auditRoms() } // Update the progress bar, indicating one more ROM has been processed - progress.setProgress(idx); + progress.incProgress(); } progress.close(); diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index e0b9a2389..61b9296cb 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -173,6 +173,36 @@ void Widget::drawChain() } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::setPosX(int x) +{ + setPos(x, _y); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::setPosY(int y) +{ + setPos(_x, y); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::setPos(int x, int y) +{ + setPos(Common::Point(x, y)); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Widget::setPos(const Common::Point& pos) +{ + if(pos != Common::Point(_x, _y)) + { + _x = pos.x; + _y = pos.y; + // we have to redraw the whole dialog! + dialog().setDirty(); + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Widget::handleMouseEntered() { diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 64685813a..cf4637ea1 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -53,6 +53,10 @@ class Widget : public GuiObject virtual int getTop() const { return _y; } virtual int getRight() const { return _x + getWidth(); } virtual int getBottom() const { return _y + getHeight(); } + virtual void setPosX(int x); + virtual void setPosY(int y); + virtual void setPos(int x, int y); + virtual void setPos(const Common::Point& pos); virtual bool handleText(char text) { return false; } virtual bool handleKeyDown(StellaKey key, StellaMod mod) { return false; } From c51d4846d8193cdad75e65ff10904a3ff91e31f3 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Mon, 23 Nov 2020 17:39:43 -0330 Subject: [PATCH 092/107] Fix compile warning and error in clang. --- src/gui/FileListWidget.cxx | 2 +- src/gui/FileListWidget.hxx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/gui/FileListWidget.cxx b/src/gui/FileListWidget.cxx index be68d632f..c0a889f1b 100644 --- a/src/gui/FileListWidget.cxx +++ b/src/gui/FileListWidget.cxx @@ -75,7 +75,7 @@ void FileListWidget::setLocation(const FilesystemNode& node, { progress().resetProgress(); progress().open(); - class FilesystemNode::CancelCheck isCancelled = []() { + FilesystemNode::CancelCheck isCancelled = []() { return myProgressDialog->isCancelled(); }; diff --git a/src/gui/FileListWidget.hxx b/src/gui/FileListWidget.hxx index 98c149223..63c39136f 100644 --- a/src/gui/FileListWidget.hxx +++ b/src/gui/FileListWidget.hxx @@ -69,7 +69,6 @@ class FileListWidget : public StringListWidget @param node The directory to display. If this is a file, its parent will instead be used, and the file will be selected @param select An optional entry to select (if applicable) - @param recursive Recursively list sub-directories too */ void setDirectory(const FilesystemNode& node, const string& select = EmptyString); From bc3c8518a2db76794ed95485c0489163b3dff38a Mon Sep 17 00:00:00 2001 From: thrust26 Date: Tue, 24 Nov 2020 12:50:43 +0100 Subject: [PATCH 093/107] added path info to launcher tooltips when displaying sub directories fixed launcher files list when filtering was canceled added persisting 'incl. subdirectories' setting --- src/emucore/FBSurface.cxx | 2 +- src/emucore/Settings.cxx | 2 ++ src/gui/FileListWidget.cxx | 48 +++++++++++++++++++++++++++++------- src/gui/FileListWidget.hxx | 5 ++++ src/gui/LauncherDialog.cxx | 28 +++++++++++---------- src/gui/StringListWidget.hxx | 4 +-- 6 files changed, 63 insertions(+), 26 deletions(-) diff --git a/src/emucore/FBSurface.cxx b/src/emucore/FBSurface.cxx index bd7a3ad7b..16030b579 100644 --- a/src/emucore/FBSurface.cxx +++ b/src/emucore/FBSurface.cxx @@ -336,7 +336,7 @@ void FBSurface::splitString(const GUI::Font& font, const string& s, int w, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool FBSurface::isWhiteSpace(const char s) const { - const string WHITESPACES = " ,.;:+-*/'([\n"; + const string WHITESPACES = " ,.;:+-*/\\'([\n"; for(size_t i = 0; i < WHITESPACES.length(); ++i) if(s == WHITESPACES[i]) diff --git a/src/emucore/Settings.cxx b/src/emucore/Settings.cxx index f7fa27ae8..a074f5697 100644 --- a/src/emucore/Settings.cxx +++ b/src/emucore/Settings.cxx @@ -145,6 +145,7 @@ Settings::Settings() setPermanent("launcherres", Common::Size(900, 600)); setPermanent("launcherfont", "medium"); setPermanent("launcherroms", "true"); + setPermanent("launchersubdirs", "false"); setPermanent("romviewer", "1"); setPermanent("lastrom", ""); @@ -542,6 +543,7 @@ void Settings::usage() const << " large12|large14|\n" << " large16>\n" << " -launcherroms <1|0> Show only ROMs in the launcher (vs. all files)\n" + << " -launchersubdirs <1|0> Show files from subdirectories too\n" << " -romviewer Show ROM info viewer at given zoom level in ROM\n" << " launcher (use 0 for off)\n" << " -followlauncher <0|1> Default ROM path follows launcher navigation\n" diff --git a/src/gui/FileListWidget.cxx b/src/gui/FileListWidget.cxx index c0a889f1b..6aaa9be1c 100644 --- a/src/gui/FileListWidget.cxx +++ b/src/gui/FileListWidget.cxx @@ -75,7 +75,7 @@ void FileListWidget::setLocation(const FilesystemNode& node, { progress().resetProgress(); progress().open(); - FilesystemNode::CancelCheck isCancelled = []() { + class FilesystemNode::CancelCheck isCancelled = []() { return myProgressDialog->isCancelled(); }; @@ -96,17 +96,26 @@ void FileListWidget::setLocation(const FilesystemNode& node, _node.getChildren(_fileList, _fsmode, _filter, false, true, isCancelled); } - if(!isCancelled()) - { - // Now fill the list widget with the names from the file list - StringList l; - for(const auto& file : _fileList) - l.push_back(file.getName()); + // Now fill the list widget with the names from the file list, + // even if cancelled + StringList l; + size_t orgLen = node.getShortPath().length(); - setList(l); - setSelected(select); + _dirList.clear(); + for(const auto& file : _fileList) + { + const string path = file.getShortPath(); + + l.push_back(file.getName()); + // display only relative path in tooltip + if(path.length() >= orgLen) + _dirList.push_back(path.substr(orgLen)); + else + _dirList.push_back(path); } + setList(l); + setSelected(select); ListWidget::recalc(); progress().close(); @@ -237,6 +246,27 @@ void FileListWidget::handleCommand(CommandSender* sender, int cmd, int data, int setTarget(this); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string FileListWidget::getToolTip(const Common::Point& pos) const +{ + Common::Rect rect = getEditRect(); + int idx = getToolTipIndex(pos); + + if(idx < 0) + return EmptyString; + + if(_includeSubDirs && _dirList.size() > idx) + return _toolTipText + _dirList[idx]; + + const string value = _list[idx]; + + if(uInt32(_font.getStringWidth(value)) > rect.w()) + return _toolTipText + value; + else + return _toolTipText; +} + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - uInt64 FileListWidget::_QUICK_SELECT_DELAY = 300; diff --git a/src/gui/FileListWidget.hxx b/src/gui/FileListWidget.hxx index 63c39136f..088ed56b3 100644 --- a/src/gui/FileListWidget.hxx +++ b/src/gui/FileListWidget.hxx @@ -53,6 +53,8 @@ class FileListWidget : public StringListWidget int x, int y, int w, int h); ~FileListWidget() override = default; + string getToolTip(const Common::Point& pos) const override; + /** Determines how to display files/folders; either setDirectory or reload must be called after any of these are called. */ void setListMode(FilesystemNode::ListMode mode) { _fsmode = mode; } @@ -69,6 +71,7 @@ class FileListWidget : public StringListWidget @param node The directory to display. If this is a file, its parent will instead be used, and the file will be selected @param select An optional entry to select (if applicable) + @param recursive Recursively list sub-directories too */ void setDirectory(const FilesystemNode& node, const string& select = EmptyString); @@ -112,6 +115,8 @@ class FileListWidget : public StringListWidget FSList _fileList; bool _includeSubDirs{false}; + StringList _dirList; + Common::FixedStack _history; uInt32 _selected{0}; string _selectedFile; diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 87d0938b4..ca4101601 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -15,9 +15,6 @@ // this file, and for a DISCLAIMER OF ALL WARRANTIES. //============================================================================ -// TODO: -// - abort current file list reload when typing - #include "bspf.hxx" #include "Bankswitch.hxx" #include "BrowserDialog.hxx" @@ -90,10 +87,10 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, int lwSelect = font.getStringWidth(lblSelect); int cwAllFiles = font.getStringWidth(lblAllFiles) + CheckboxWidget::prefixSize(font); - int lwFilter = font.getStringWidth(lblFilter); int cwSubDirs = font.getStringWidth(lblSubDirs) + CheckboxWidget::prefixSize(font); + int lwFilter = font.getStringWidth(lblFilter); int lwFound = font.getStringWidth(lblFound); - int wTotal = HBORDER * 2 + lwSelect + cwAllFiles + lwFilter + cwSubDirs + lwFound + int wTotal = HBORDER * 2 + lwSelect + cwAllFiles + cwSubDirs + lwFilter + lwFound + EditTextWidget::calcWidth(font, "123456") + LBL_GAP * 7; bool noSelect = false; @@ -174,13 +171,6 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, xpos - cwSubDirs - lwFilter - cwAllFiles - lwSelect - HBORDER - LBL_GAP * (noSelect ? 5 : 7)); - // Show the subdirectories checkbox - xpos -= cwSubDirs + LBL_GAP; - mySubDirs = new CheckboxWidget(this, font, xpos, ypos, lblSubDirs, kSubDirsCmd); - ostringstream tip; - tip << "Search files in subdirectories too."; - mySubDirs->setToolTip(tip.str()); - // Show the filter input field xpos -= fwFilter + LBL_GAP; myPattern = new EditTextWidget(this, font, xpos, ypos - 2, fwFilter, lineHeight, ""); @@ -191,11 +181,18 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, xpos -= lwFilter + LBL_GAP; new StaticTextWidget(this, font, xpos, ypos, lblFilter); + // Show the subdirectories checkbox + xpos -= cwSubDirs + LBL_GAP * 2; + mySubDirs = new CheckboxWidget(this, font, xpos, ypos, lblSubDirs, kSubDirsCmd); + ostringstream tip; + tip << "Search files in subdirectories too."; + mySubDirs->setToolTip(tip.str()); + // Show the checkbox for all files if(noSelect) xpos = HBORDER; else - xpos -= cwAllFiles + LBL_GAP * 2; + xpos -= cwAllFiles + LBL_GAP; myAllFiles = new CheckboxWidget(this, font, xpos, ypos, lblAllFiles, kAllfilesCmd); myAllFiles->setToolTip("Uncheck to show ROM files only."); @@ -386,6 +383,10 @@ void LauncherDialog::loadConfig() instance().settings().setValue("stella.version", STELLA_VERSION); } + bool subDirs = instance().settings().getBool("launchersubdirs"); + mySubDirs->setState(subDirs); + myList->setIncludeSubDirs(subDirs); + // Assume that if the list is empty, this is the first time that loadConfig() // has been called (and we should reload the list) if(myList->getList().empty()) @@ -408,6 +409,7 @@ void LauncherDialog::loadConfig() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void LauncherDialog::saveConfig() { + instance().settings().setValue("launchersubdirs", mySubDirs->getState()); if(instance().settings().getBool("followlauncher")) instance().settings().setValue("romdir", myList->currentDir().getShortPath()); } diff --git a/src/gui/StringListWidget.hxx b/src/gui/StringListWidget.hxx index f0bf8544b..668414c4c 100644 --- a/src/gui/StringListWidget.hxx +++ b/src/gui/StringListWidget.hxx @@ -41,6 +41,7 @@ class StringListWidget : public ListWidget void lostFocusWidget() override { setDirty(); } bool hasToolTip() const override { return true; } + int getToolTipIndex(const Common::Point& pos) const; void drawWidget(bool hilite) override; Common::Rect getEditRect() const override; @@ -49,9 +50,6 @@ class StringListWidget : public ListWidget bool _hilite{false}; int _textOfs{0}; - private: - int getToolTipIndex(const Common::Point& pos) const; - private: // Following constructors and assignment operators not supported StringListWidget() = delete; From 8f1c84b3cc5d89af1a4de81542bc7fce9219aee4 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Tue, 24 Nov 2020 10:01:27 -0330 Subject: [PATCH 094/107] Fixed compile error and warnings from g++. --- src/debugger/gui/TiaZoomWidget.hxx | 4 +++- src/gui/FileListWidget.cxx | 4 ++-- src/gui/FileListWidget.hxx | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/debugger/gui/TiaZoomWidget.hxx b/src/debugger/gui/TiaZoomWidget.hxx index d46787f23..1cc53068f 100644 --- a/src/debugger/gui/TiaZoomWidget.hxx +++ b/src/debugger/gui/TiaZoomWidget.hxx @@ -27,12 +27,14 @@ class ContextMenu; class TiaZoomWidget : public Widget, public CommandSender { public: + using Widget::setPos; + TiaZoomWidget(GuiObject *boss, const GUI::Font& font, int x, int y, int w, int h); ~TiaZoomWidget() override = default; void loadConfig() override; - void setPos(int x, int y); + void setPos(int x, int y) override; string getToolTip(const Common::Point& pos) const override; bool changedToolTip(const Common::Point& oldPos, const Common::Point& newPos) const override; diff --git a/src/gui/FileListWidget.cxx b/src/gui/FileListWidget.cxx index 6aaa9be1c..d2cff5261 100644 --- a/src/gui/FileListWidget.cxx +++ b/src/gui/FileListWidget.cxx @@ -75,7 +75,7 @@ void FileListWidget::setLocation(const FilesystemNode& node, { progress().resetProgress(); progress().open(); - class FilesystemNode::CancelCheck isCancelled = []() { + FilesystemNode::CancelCheck isCancelled = []() { return myProgressDialog->isCancelled(); }; @@ -255,7 +255,7 @@ string FileListWidget::getToolTip(const Common::Point& pos) const if(idx < 0) return EmptyString; - if(_includeSubDirs && _dirList.size() > idx) + if(_includeSubDirs && static_cast(_dirList.size()) > idx) return _toolTipText + _dirList[idx]; const string value = _list[idx]; diff --git a/src/gui/FileListWidget.hxx b/src/gui/FileListWidget.hxx index 088ed56b3..32f6517f7 100644 --- a/src/gui/FileListWidget.hxx +++ b/src/gui/FileListWidget.hxx @@ -71,7 +71,6 @@ class FileListWidget : public StringListWidget @param node The directory to display. If this is a file, its parent will instead be used, and the file will be selected @param select An optional entry to select (if applicable) - @param recursive Recursively list sub-directories too */ void setDirectory(const FilesystemNode& node, const string& select = EmptyString); From 8a2cace6c85b225f6bddc6eb5ea7d87f8e492ee9 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 25 Nov 2020 17:21:57 +0100 Subject: [PATCH 095/107] fixed #735 (trackball fire) --- src/emucore/PointingDevice.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emucore/PointingDevice.cxx b/src/emucore/PointingDevice.cxx index cbd7f89c9..b48c372a3 100644 --- a/src/emucore/PointingDevice.cxx +++ b/src/emucore/PointingDevice.cxx @@ -81,7 +81,7 @@ void PointingDevice::update() return; // Update horizontal direction - cerr << myEvent.get(Event::MouseAxisXMove) << ", " << myHCounterRemainder << endl; + //cerr << myEvent.get(Event::MouseAxisXMove) << ", " << myHCounterRemainder << endl; updateDirection( myEvent.get(Event::MouseAxisXMove), myHCounterRemainder, myTrackBallLeft, myTrackBallLinesH, myScanCountH, myFirstScanOffsetH); @@ -90,7 +90,7 @@ void PointingDevice::update() myTrackBallDown, myTrackBallLinesV, myScanCountV, myFirstScanOffsetV); // We allow left and right mouse buttons for fire button - setPin(DigitalPin::Six, !getAutoFireState(myEvent.get(Event::JoystickZeroFire) == 0 || + setPin(DigitalPin::Six, !getAutoFireState(myEvent.get(Event::JoystickZeroFire) || myEvent.get(Event::MouseButtonLeftValue) || myEvent.get(Event::MouseButtonRightValue))); } From 1282c1411d008b342384f1d24a2326f66fe6599a Mon Sep 17 00:00:00 2001 From: thrust26 Date: Wed, 25 Nov 2020 17:23:05 +0100 Subject: [PATCH 096/107] fixed doc --- docs/graphics/rominfo_1x_large.png | Bin 28882 -> 45401 bytes docs/graphics/rominfo_1x_small.png | Bin 19415 -> 36557 bytes docs/graphics/rominfo_2x_small.png | Bin 52980 -> 76225 bytes docs/index.html | 6 +++--- src/emucore/FrameBuffer.cxx | 3 ++- src/gui/ProgressDialog.cxx | 2 +- src/gui/TimeLineWidget.cxx | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/graphics/rominfo_1x_large.png b/docs/graphics/rominfo_1x_large.png index 99fee27d28ed56513c134a6d60d39ae67299d18f..5a01334046ab195f6b983a5077581135c1e8cb29 100644 GIT binary patch literal 45401 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�KP>XO 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 diff --git a/docs/graphics/rominfo_1x_small.png b/docs/graphics/rominfo_1x_small.png index 4d6429409df619219390778884f73515b3afcc5a..99d23e135be7bca145549ed99477c3d58bba1d7c 100644 GIT binary patch literal 36557 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%0D}y@og`N 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 literal 52980 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∓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

    x4ir2PL1;&G{CcAIYNS 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 The 'Show all files' checkbox allows displaying files which do not have a valid ROM extension. +

  • + If 'Incl. subdirectories' is checked, Stella will list matching files + from all subdirectories too.
  • The 'Filter' text box can be used to narrow down the results in the ROM listing. When this box is empty, all files are shown. Typing @@ -3706,9 +3709,6 @@ about capital or lower-case letters. You also can use '*' and '?' as wildcards. E.g. for '(198?)*atari' only ROMs from the 1980s made by Atari will be listed. -
  • - If 'Incl. subdirectories' is checked, Stella will list matching files - from all subdirectories too.

  • diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index f9ede53ab..2171382c5 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -707,7 +707,8 @@ inline bool FrameBuffer::drawMessage() if(myMsg.dirty) { - cerr << "--- draw message ---" << endl; + cerr << "m"; + //cerr << "--- draw message ---" << endl; // Draw the bounded box and text const Common::Rect& dst = myMsg.surface->dstRect(); diff --git a/src/gui/ProgressDialog.cxx b/src/gui/ProgressDialog.cxx index e36cf6008..80aafc8c4 100644 --- a/src/gui/ProgressDialog.cxx +++ b/src/gui/ProgressDialog.cxx @@ -86,7 +86,7 @@ void ProgressDialog::setMessage(const string& message) myMessage->setLabel(message); mySlider->setWidth(lwidth); - _cancelWidget->setPos((_w - buttonWidth) / 2, _cancelWidget->getTop()); + _cancelWidget->setPosX((_w - buttonWidth) / 2); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/TimeLineWidget.cxx b/src/gui/TimeLineWidget.cxx index 4e6b9e9a0..5f6e0e687 100644 --- a/src/gui/TimeLineWidget.cxx +++ b/src/gui/TimeLineWidget.cxx @@ -143,7 +143,7 @@ void TimeLineWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); - cerr << "TimeLineWidget::drawWidget " << typeid(s).name() << endl; + //cerr << "TimeLineWidget::drawWidget " << typeid(s).name() << endl; // Draw the label, if any if(_labelWidth > 0) From a8e83b63edabd7eee02760d258c3b6ff96931352 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 28 Nov 2020 12:54:20 +0100 Subject: [PATCH 097/107] updated changes.txt --- Changes.txt | 2 ++ src/gui/WhatsNewDialog.cxx | 1 + 2 files changed, 3 insertions(+) diff --git a/Changes.txt b/Changes.txt index 51992c85b..c2767093b 100644 --- a/Changes.txt +++ b/Changes.txt @@ -28,6 +28,8 @@ * Increased sample size for CDFJ+. + * Fixed autofire bug for trackball controllers. + -Have fun! diff --git a/src/gui/WhatsNewDialog.cxx b/src/gui/WhatsNewDialog.cxx index 7c26ca186..4baa36101 100644 --- a/src/gui/WhatsNewDialog.cxx +++ b/src/gui/WhatsNewDialog.cxx @@ -45,6 +45,7 @@ WhatsNewDialog::WhatsNewDialog(OSystem& osystem, DialogContainer& parent, const #if defined(RETRON77) add(ypos, "increased sample size for CDFJ+"); add(ypos, "fixed navigation bug in Video & Audio settings dialog"); + add(ypos, "fixed autofire bug for trackball controllers"); #else add(ypos, "enhanced cut/copy/paste for text editing"); add(ypos, "added undo and redo to text editing"); From 853370913950b6f17efd61a6e8944af22b30aa7d Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 28 Nov 2020 12:55:50 +0100 Subject: [PATCH 098/107] added some more tooltips to UI --- src/cheat/CheatCodeDialog.cxx | 1 + src/gui/EmulationDialog.cxx | 1 + src/gui/EventMappingWidget.cxx | 3 +++ src/gui/UIDialog.cxx | 5 +++++ 4 files changed, 10 insertions(+) diff --git a/src/cheat/CheatCodeDialog.cxx b/src/cheat/CheatCodeDialog.cxx index 09ac1cd3c..481dcbcd5 100644 --- a/src/cheat/CheatCodeDialog.cxx +++ b/src/cheat/CheatCodeDialog.cxx @@ -100,6 +100,7 @@ CheatCodeDialog::CheatCodeDialog(OSystem& osystem, DialogContainer& parent, return (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); }; myCheatInput->setTextFilter(f1, 1); + myCheatInput->setToolTip("See Stella documentation for details.", 1); addToFocusList(wid); diff --git a/src/gui/EmulationDialog.cxx b/src/gui/EmulationDialog.cxx index dcbb42859..a2c0bee9c 100644 --- a/src/gui/EmulationDialog.cxx +++ b/src/gui/EmulationDialog.cxx @@ -99,6 +99,7 @@ EmulationDialog::EmulationDialog(OSystem& osystem, DialogContainer& parent, // Use sync to vblank myUseVSync = new CheckboxWidget(this, _font, xpos, ypos + 1, "VSync"); + myUseVSync->setToolTip("Check to enable vertical synced display updates."); wid.push_back(myUseVSync); ypos += lineHeight + VGAP; diff --git a/src/gui/EventMappingWidget.cxx b/src/gui/EventMappingWidget.cxx index 8051e4a56..7b3ae540b 100644 --- a/src/gui/EventMappingWidget.cxx +++ b/src/gui/EventMappingWidget.cxx @@ -97,6 +97,7 @@ EventMappingWidget::EventMappingWidget(GuiObject* boss, const GUI::Font& font, myCancelMapButton = new ButtonWidget(boss, font, xpos, ypos, buttonWidth, buttonHeight, "Cancel", kStopMapCmd); + myCancelMapButton->setToolTip("Cancel current mapping."); myCancelMapButton->setTarget(this); myCancelMapButton->clearFlags(Widget::FLAG_ENABLED); addFocusWidget(myCancelMapButton); @@ -106,12 +107,14 @@ EventMappingWidget::EventMappingWidget(GuiObject* boss, const GUI::Font& font, buttonWidth, buttonHeight, "Erase", kEraseCmd); myEraseButton->setTarget(this); + myEraseButton->setToolTip("Erase any mapping for selected event."); addFocusWidget(myEraseButton); ypos += buttonHeight + VGAP; myResetButton = new ButtonWidget(boss, font, xpos, ypos, buttonWidth, buttonHeight, "Reset", kResetCmd); + myResetButton->setToolTip("Reset mapping for selected event to defaults."); myResetButton->setTarget(this); addFocusWidget(myResetButton); diff --git a/src/gui/UIDialog.cxx b/src/gui/UIDialog.cxx index 12f710bc0..2d773b888 100644 --- a/src/gui/UIDialog.cxx +++ b/src/gui/UIDialog.cxx @@ -109,6 +109,8 @@ UIDialog::UIDialog(OSystem& osystem, DialogContainer& parent, xpos = myDialogFontPopup->getRight() + fontWidth * 5; myHidpiWidget = new CheckboxWidget(myTab, font, xpos, ypos + 1, "HiDPI mode (*)"); + myHidpiWidget->setToolTip("Scale the UI by a factor of two when enabled."); + wid.push_back(myHidpiWidget); // Dialog position @@ -126,6 +128,7 @@ UIDialog::UIDialog(OSystem& osystem, DialogContainer& parent, // Center window (in windowed mode) xpos = myHidpiWidget->getLeft(); myCenter = new CheckboxWidget(myTab, _font, xpos, ypos + 1, "Center windows"); + myCenter->setToolTip("Check to center all windows, else remember last position."); wid.push_back(myCenter); // Delay between quick-selecting characters in ListWidget @@ -138,6 +141,8 @@ UIDialog::UIDialog(OSystem& osystem, DialogContainer& parent, myListDelaySlider->setMaxValue(1000); myListDelaySlider->setStepValue(50); myListDelaySlider->setTickmarkIntervals(5); + myListDelaySlider->setToolTip("Set delay between key presses in file lists" + " before a search string resets."); wid.push_back(myListDelaySlider); ypos += lineHeight + VGAP; From c1ddf81b825d1437ad4f0321edbbe5d2ccb43a10 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 28 Nov 2020 12:57:24 +0100 Subject: [PATCH 099/107] improved debugger's RAM labels --- src/debugger/CartDebug.cxx | 63 +++++++++++++++++++++++++------------- src/debugger/CartDebug.hxx | 6 ++-- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/debugger/CartDebug.cxx b/src/debugger/CartDebug.cxx index d62b16b66..ab988e000 100644 --- a/src/debugger/CartDebug.cxx +++ b/src/debugger/CartDebug.cxx @@ -605,7 +605,8 @@ bool CartDebug::removeLabel(const string& label) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool CartDebug::getLabel(ostream& buf, uInt16 addr, bool isRead, int places) const +bool CartDebug::getLabel(ostream& buf, uInt16 addr, bool isRead, + int places, bool isRam) const { switch(addressType(addr)) { @@ -662,21 +663,24 @@ bool CartDebug::getLabel(ostream& buf, uInt16 addr, bool isRead, int places) con { // RAM can use user-defined labels; otherwise we default to // standard mnemonics - auto iter = myUserLabels.find(addr); - if(iter != myUserLabels.end()) - { - buf << iter->second; - } - else - { - uInt16 a = addr & 0xFF, offset = addr & 0xFF00; - if((iter = myUserLabels.find(a)) != myUserLabels.end()) + AddrToLabel::const_iterator iter; + uInt16 a = addr & 0xFF, offset = addr & 0xFF00; + bool found = false; + + // Search for nearest label + for(uInt16 i = a; i >= 0x80; --i) + if((iter = myUserLabels.find(i)) != myUserLabels.end()) + { buf << iter->second; - else - buf << ourZPMnemonic[a - 0x80]; - if(offset > 0) - buf << "|$" << Base::HEX2 << offset; - } + if(a != i) + buf << "+$" << Base::HEX1 << (a - i); + found = true; + break; + } + if(!found) + buf << ourZPMnemonic[a - 0x80]; + if(offset > 0) + buf << "|$" << Base::HEX2 << offset; return true; } @@ -684,11 +688,28 @@ bool CartDebug::getLabel(ostream& buf, uInt16 addr, bool isRead, int places) con case AddrType::ROM: { // These addresses can never be in the system labels list - const auto& iter = myUserLabels.find(addr); - if(iter != myUserLabels.end()) + if(isRam) // cartridge RAM { - buf << iter->second; - return true; + AddrToLabel::const_iterator iter; + + // Search for nearest label + for(uInt16 i = addr; i >= (addr & 0xf000); --i) + if((iter = myUserLabels.find(i)) != myUserLabels.end()) + { + buf << iter->second; + if(addr != i) + buf << "+$" << Base::HEX1 << (addr - i); + return true; + } + } + else + { + const auto& iter = myUserLabels.find(addr); + if(iter != myUserLabels.end()) + { + buf << iter->second; + return true; + } } break; } @@ -713,10 +734,10 @@ bool CartDebug::getLabel(ostream& buf, uInt16 addr, bool isRead, int places) con } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -string CartDebug::getLabel(uInt16 addr, bool isRead, int places) const +string CartDebug::getLabel(uInt16 addr, bool isRead, int places, bool isRam) const { ostringstream buf; - getLabel(buf, addr, isRead, places); + getLabel(buf, addr, isRead, places, isRam); return buf.str(); } diff --git a/src/debugger/CartDebug.hxx b/src/debugger/CartDebug.hxx index 9d71de51b..25dd59e63 100644 --- a/src/debugger/CartDebug.hxx +++ b/src/debugger/CartDebug.hxx @@ -211,8 +211,10 @@ class CartDebug : public DebuggerSystem If places is not -1 and a label hasn't been defined, return a formatted hexidecimal address */ - bool getLabel(ostream& buf, uInt16 addr, bool isRead, int places = -1) const; - string getLabel(uInt16 addr, bool isRead, int places = -1) const; + bool getLabel(ostream& buf, uInt16 addr, bool isRead, + int places = -1, bool isRam = false) const; + string getLabel(uInt16 addr, bool isRead, + int places = -1, bool isRam = false) const; int getAddress(const string& label) const; /** From 038557ba69ee3720800a5ae3821c64d1c60e2724 Mon Sep 17 00:00:00 2001 From: Christian Speckner Date: Sat, 28 Nov 2020 12:59:07 +0100 Subject: [PATCH 100/107] Fix bad use of constexpr. --- src/gui/ToolTip.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index a887e0096..12ede067a 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -123,12 +123,13 @@ void ToolTip::release(bool emptyTip) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToolTip::show(const string& tip) { + uInt32 maxRows = MAX_ROWS; myTipPos = myMousePos; uInt32 maxWidth = std::min(myWidth - myTextXOfs * 2, uInt32(myFont->getStringWidth(tip))); mySurface->fillRect(1, 1, maxWidth + myTextXOfs * 2 - 2, myHeight - 2, kWidColor); - int lines = std::min(MAX_ROWS, + int lines = std::min(maxRows, uInt32(mySurface->drawString(*myFont, tip, myTextXOfs, myTextYOfs, maxWidth, myHeight - myTextYOfs * 2, kTextColor))); From 5711d5cec39a902f98836613894d0e5f9019043d Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 28 Nov 2020 12:59:45 +0100 Subject: [PATCH 101/107] improved debugger's RAM labels (part 2) --- src/debugger/gui/CartEnhancedWidget.cxx | 2 +- src/debugger/gui/DataGridRamWidget.cxx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/debugger/gui/CartEnhancedWidget.cxx b/src/debugger/gui/CartEnhancedWidget.cxx index 97beb1f6b..80da02ffd 100644 --- a/src/debugger/gui/CartEnhancedWidget.cxx +++ b/src/debugger/gui/CartEnhancedWidget.cxx @@ -392,5 +392,5 @@ uInt8 CartridgeEnhancedWidget::internalRamGetValue(int addr) string CartridgeEnhancedWidget::internalRamLabel(int addr) { CartDebug& dbg = instance().debugger().cartDebug(); - return dbg.getLabel(addr + ADDR_BASE + myCart.myReadOffset, false); + return dbg.getLabel(addr + ADDR_BASE + myCart.myReadOffset, false, -1, true); } diff --git a/src/debugger/gui/DataGridRamWidget.cxx b/src/debugger/gui/DataGridRamWidget.cxx index 9be4c4f14..7445f1d90 100644 --- a/src/debugger/gui/DataGridRamWidget.cxx +++ b/src/debugger/gui/DataGridRamWidget.cxx @@ -48,7 +48,7 @@ string DataGridRamWidget::getToolTip(const Common::Point& pos) const ostringstream buf; - buf << _ram.getLabel(addr) << '\n' << tip; + buf << label << '\n' << tip; return buf.str(); } From f0c599bfe4e13b65aa5ae0c5a17615c944031555 Mon Sep 17 00:00:00 2001 From: Christian Speckner Date: Sat, 28 Nov 2020 13:32:29 +0100 Subject: [PATCH 102/107] Fix null pointer. --- src/gui/LauncherDialog.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index ca4101601..4a8f437d4 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -384,7 +384,7 @@ void LauncherDialog::loadConfig() } bool subDirs = instance().settings().getBool("launchersubdirs"); - mySubDirs->setState(subDirs); + if (mySubDirs) mySubDirs->setState(subDirs); myList->setIncludeSubDirs(subDirs); // Assume that if the list is empty, this is the first time that loadConfig() From 41bb891cc0eb0eb04095b42a833dbe6253852453 Mon Sep 17 00:00:00 2001 From: Christian Speckner Date: Sat, 28 Nov 2020 13:32:57 +0100 Subject: [PATCH 103/107] Avoid endless loop and heap corruption of doom. --- src/gui/FileListWidget.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/FileListWidget.cxx b/src/gui/FileListWidget.cxx index d2cff5261..613b05811 100644 --- a/src/gui/FileListWidget.cxx +++ b/src/gui/FileListWidget.cxx @@ -51,7 +51,7 @@ void FileListWidget::setDirectory(const FilesystemNode& node, // Initialize history FilesystemNode tmp = _node; - while(tmp.hasParent()) + while(tmp.hasParent() && !_history.full()) { string name = tmp.getName(); if(name.back() == '/' || name.back() == '\\') From b7943546796db5269006838f7734051c52e69048 Mon Sep 17 00:00:00 2001 From: Stephen Anthony Date: Sat, 28 Nov 2020 10:48:00 -0330 Subject: [PATCH 104/107] Move Linux builds to use C++17 by default. --- Changes.txt | 2 ++ Makefile | 4 ++-- configure | 2 +- docs/index.html | 4 ++-- src/libretro/Makefile | 6 +++--- src/libretro/jni/Android.mk | 2 +- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Changes.txt b/Changes.txt index c2767093b..6140d6f59 100644 --- a/Changes.txt +++ b/Changes.txt @@ -30,6 +30,8 @@ * Fixed autofire bug for trackball controllers. + * Codebase now uses C++17 features. + -Have fun! diff --git a/Makefile b/Makefile index 87a2c9847..d9dc607c9 100644 --- a/Makefile +++ b/Makefile @@ -48,11 +48,11 @@ endif CXXFLAGS+= -Wall -Wextra -Wno-unused-parameter ifdef HAVE_GCC - CXXFLAGS+= -Wno-multichar -Wunused -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 + CXXFLAGS+= -Wno-multichar -Wunused -Woverloaded-virtual -Wnon-virtual-dtor -std=c++17 endif ifdef HAVE_CLANG - CXXFLAGS+= -Wno-multichar -Wunused -Woverloaded-virtual -Wnon-virtual-dtor -std=c++14 + CXXFLAGS+= -Wno-multichar -Wunused -Woverloaded-virtual -Wnon-virtual-dtor -std=c++17 endif ifdef CLANG_WARNINGS diff --git a/configure b/configure index 31281ec4c..ad2ebbed7 100755 --- a/configure +++ b/configure @@ -385,7 +385,7 @@ else fi for compiler in $compilers; do - if test_compiler "$compiler -std=c++14"; then + if test_compiler "$compiler -std=c++17"; then CXX=$compiler echo $CXX break diff --git a/docs/index.html b/docs/index.html index 0c6598b56..369598e23 100644 --- a/docs/index.html +++ b/docs/index.html @@ -260,7 +260,7 @@
      -
    • High speed emulation using optimized C++14 code
    • +
    • High speed emulation using optimized C++17 code
    • Supports high quality TIA emulation using the cycle-exact TIA core from 6502.ts by Christian Speckner
    • @@ -350,7 +350,7 @@
    • OpenGL capable video card
    • Other architectures (MIPS, PPC, PPC64, etc.) have been confirmed to work, but aren't as well tested as i386/x86_64
    • -
    • GNU g++ v/6 or Clang v/3.9 (with C++14 support) and the make utility are required for compiling the Stella source code
    • +
    • GNU g++ v/7 or Clang v/5 (with C++17 support) and the make utility are required for compiling the Stella source code

    diff --git a/src/libretro/Makefile b/src/libretro/Makefile index 8d17db9db..6f8eb6599 100644 --- a/src/libretro/Makefile +++ b/src/libretro/Makefile @@ -43,7 +43,7 @@ TARGET_NAME = stella ifeq (,$(findstring msvc,$(platform))) - CXXFLAGS += -std=c++14 -fno-rtti + CXXFLAGS += -std=c++17 -fno-rtti LIBS := -lm else LIBS := @@ -282,8 +282,8 @@ else ifneq (,$(filter $(platform), ngc wii wiiu)) else ifeq ($(platform), emscripten) TARGET := $(TARGET_NAME)_libretro_$(platform).bc STATIC_LINKING = 1 - CXXFLAGS += $(PTHREAD_FLAGS) -std=c++14 - LDFLAGS += $(PTHREAD_FLAGS) -std=c++14 + CXXFLAGS += $(PTHREAD_FLAGS) -std=c++17 + LDFLAGS += $(PTHREAD_FLAGS) -std=c++17 # Genode else ifeq ($(platform), genode) diff --git a/src/libretro/jni/Android.mk b/src/libretro/jni/Android.mk index 3115baa53..644f983ae 100644 --- a/src/libretro/jni/Android.mk +++ b/src/libretro/jni/Android.mk @@ -14,6 +14,6 @@ endif include $(CLEAR_VARS) LOCAL_MODULE := retro LOCAL_SRC_FILES := $(SOURCES_CXX) $(SOURCES_C) -LOCAL_CXXFLAGS := $(COREFLAGS) -std=c++14 +LOCAL_CXXFLAGS := $(COREFLAGS) -std=c++17 LOCAL_LDFLAGS := -Wl,-version-script=$(CORE_DIR)/libretro/link.T include $(BUILD_SHARED_LIBRARY) From b3576478193e79120497f13086e851c7275bae44 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 28 Nov 2020 16:12:54 +0100 Subject: [PATCH 105/107] minimized redraws in debugger --- src/debugger/gui/DataGridWidget.cxx | 51 ++++++++++++++++++-------- src/debugger/gui/DataGridWidget.hxx | 2 +- src/debugger/gui/DebuggerDialog.cxx | 12 ------ src/debugger/gui/RomListWidget.cxx | 3 +- src/debugger/gui/TiaInfoWidget.cxx | 6 ++- src/debugger/gui/ToggleBitWidget.cxx | 6 ++- src/debugger/gui/TogglePixelWidget.cxx | 19 ++++++++-- src/debugger/gui/TogglePixelWidget.hxx | 2 +- src/gui/ColorWidget.cxx | 17 ++++++++- src/gui/ColorWidget.hxx | 2 +- src/gui/EditTextWidget.cxx | 6 ++- src/gui/EditableWidget.cxx | 8 ++-- src/gui/PopUpWidget.cxx | 11 +++++- src/gui/ScrollBarWidget.cxx | 6 ++- src/gui/TabWidget.cxx | 5 ++- src/gui/Widget.cxx | 20 +++++----- 16 files changed, 117 insertions(+), 59 deletions(-) diff --git a/src/debugger/gui/DataGridWidget.cxx b/src/debugger/gui/DataGridWidget.cxx index 20489912c..9fa82bcdc 100644 --- a/src/debugger/gui/DataGridWidget.cxx +++ b/src/debugger/gui/DataGridWidget.cxx @@ -107,15 +107,21 @@ DataGridWidget::DataGridWidget(GuiObject* boss, const GUI::Font& font, void DataGridWidget::setList(const IntArray& alist, const IntArray& vlist, const BoolArray& changed) { -/* -cerr << "alist.size() = " << alist.size() - << ", vlist.size() = " << vlist.size() - << ", changed.size() = " << changed.size() - << ", _rows*_cols = " << _rows * _cols << endl << endl; -*/ + /* + cerr << "alist.size() = " << alist.size() + << ", vlist.size() = " << vlist.size() + << ", changed.size() = " << changed.size() + << ", _rows*_cols = " << _rows * _cols << endl << endl; + */ int size = int(vlist.size()); // assume the alist is the same size assert(size == _rows * _cols); + bool dirty = _editMode + || !std::equal(_valueList.begin(), _valueList.end(), + vlist.begin(), vlist.end()) + || !std::equal(_changedList.begin(), _changedList.end(), + changed.begin(), changed.end()); + _addrList.clear(); _valueList.clear(); _valueStringList.clear(); @@ -130,19 +136,22 @@ cerr << "alist.size() = " << alist.size() for(int i = 0; i < size; ++i) _valueStringList.push_back(Common::Base::toString(_valueList[i], _base)); -/* -cerr << "_addrList.size() = " << _addrList.size() - << ", _valueList.size() = " << _valueList.size() - << ", _changedList.size() = " << _changedList.size() - << ", _valueStringList.size() = " << _valueStringList.size() - << ", _rows*_cols = " << _rows * _cols << endl << endl; -*/ + /* + cerr << "_addrList.size() = " << _addrList.size() + << ", _valueList.size() = " << _valueList.size() + << ", _changedList.size() = " << _changedList.size() + << ", _valueStringList.size() = " << _valueStringList.size() + << ", _rows*_cols = " << _rows * _cols << endl << endl; + */ enableEditMode(false); - // Send item selected signal for starting with cell 0 - sendCommand(DataGridWidget::kSelectionChangedCmd, _selectedItem, _id); + if(dirty) + { + // Send item selected signal for starting with cell 0 + sendCommand(DataGridWidget::kSelectionChangedCmd, _selectedItem, _id); - setDirty(); + setDirty(); + } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -708,6 +717,16 @@ int DataGridWidget::getWidth() const return _w + (_scrollBar ? ScrollBarWidget::scrollBarWidth(_font) : 0); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void DataGridWidget::setCrossed(bool enable) +{ + if(_crossGrid != enable) + { + _crossGrid = enable; + setDirty(); + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DataGridWidget::startEditMode() { diff --git a/src/debugger/gui/DataGridWidget.hxx b/src/debugger/gui/DataGridWidget.hxx index 848993311..ed1b6be02 100644 --- a/src/debugger/gui/DataGridWidget.hxx +++ b/src/debugger/gui/DataGridWidget.hxx @@ -82,7 +82,7 @@ class DataGridWidget : public EditableWidget void setOpsWidget(DataGridOpsWidget* w) { _opsWidget = w; } - void setCrossed(bool enable) { _crossGrid = enable; } + void setCrossed(bool enable); string getToolTip(const Common::Point& pos) const override; bool changedToolTip(const Common::Point& oldPos, const Common::Point& newPos) const override; diff --git a/src/debugger/gui/DebuggerDialog.cxx b/src/debugger/gui/DebuggerDialog.cxx index b8a28f579..f2ac5c230 100644 --- a/src/debugger/gui/DebuggerDialog.cxx +++ b/src/debugger/gui/DebuggerDialog.cxx @@ -272,84 +272,72 @@ void DebuggerDialog::handleCommand(CommandSender* sender, int cmd, void DebuggerDialog::doStep() { instance().debugger().parser().run("step"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doTrace() { instance().debugger().parser().run("trace"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doAdvance() { instance().debugger().parser().run("frame #1"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doScanlineAdvance() { instance().debugger().parser().run("scanline #1"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doRewind() { instance().debugger().parser().run("rewind"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doUnwind() { instance().debugger().parser().run("unwind"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doRewind10() { instance().debugger().parser().run("rewind #10"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doUnwind10() { instance().debugger().parser().run("unwind #10"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doRewindAll() { instance().debugger().parser().run("rewind #1000"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doUnwindAll() { instance().debugger().parser().run("unwind #1000"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doExitDebugger() { instance().debugger().parser().run("run"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DebuggerDialog::doExitRom() { instance().debugger().parser().run("exitrom"); - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/debugger/gui/RomListWidget.cxx b/src/debugger/gui/RomListWidget.cxx index c226398a6..9f8b9d589 100644 --- a/src/debugger/gui/RomListWidget.cxx +++ b/src/debugger/gui/RomListWidget.cxx @@ -553,7 +553,8 @@ void RomListWidget::drawWidget(bool hilite) checkBreakPoint(dlist[pos].address, instance().debugger().cartDebug().getBank(dlist[pos].address))); myCheckList[i]->setDirty(); - // draw immediately, because chain order is not deterministic + // All checkboxes have to be redrawn because RomListWidget clears its whole area + // Also draw immediately, because chain order is not deterministic myCheckList[i]->draw(); // Draw highlighted item in a frame diff --git a/src/debugger/gui/TiaInfoWidget.cxx b/src/debugger/gui/TiaInfoWidget.cxx index 2690f8b4f..fe2ce6d9b 100644 --- a/src/debugger/gui/TiaInfoWidget.cxx +++ b/src/debugger/gui/TiaInfoWidget.cxx @@ -81,7 +81,6 @@ TiaInfoWidget::TiaInfoWidget(GuiObject* boss, const GUI::Font& lfont, ypos += lineHeight + VGAP; new StaticTextWidget(boss, lfont, xpos, ypos + 1, "Total"); myTotalCycles = new EditTextWidget(boss, nfont, xpos + lwidth8, ypos - 1, twidth, lineHeight); - myTotalCycles->setToolTip("Total CPU cycles executed for this session (E notation)."); myTotalCycles->setEditable(false, true); // Left: Delta Cycles @@ -170,7 +169,10 @@ void TiaInfoWidget::loadConfig() uInt64 total = tia.cyclesLo() + (uInt64(tia.cyclesHi()) << 32); uInt64 totalOld = oldTia.info[2] + (uInt64(oldTia.info[3]) << 32); myTotalCycles->setText(Common::Base::toString(uInt32(total) / 1000000, Common::Base::Fmt::_10_6) + "e6", - total != totalOld); + total / 1000000 != totalOld / 1000000); + myTotalCycles->setToolTip("Total CPU cycles (E notation) executed for this session (" + + std::to_string(total) + ")."); + uInt64 delta = total - totalOld; myDeltaCycles->setText(Common::Base::toString(uInt32(delta), Common::Base::Fmt::_10_8)); // no coloring diff --git a/src/debugger/gui/ToggleBitWidget.cxx b/src/debugger/gui/ToggleBitWidget.cxx index 994d73aa9..629e58c57 100644 --- a/src/debugger/gui/ToggleBitWidget.cxx +++ b/src/debugger/gui/ToggleBitWidget.cxx @@ -70,12 +70,14 @@ void ToggleBitWidget::setList(const StringList& off, const StringList& on) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToggleBitWidget::setState(const BoolArray& state, const BoolArray& changed) { + if(!std::equal(_changedList.begin(), _changedList.end(), + changed.begin(), changed.end())) + setDirty(); + _stateList.clear(); _stateList = state; _changedList.clear(); _changedList = changed; - - setDirty(); } diff --git a/src/debugger/gui/TogglePixelWidget.cxx b/src/debugger/gui/TogglePixelWidget.cxx index 1cdb80bae..c26600130 100644 --- a/src/debugger/gui/TogglePixelWidget.cxx +++ b/src/debugger/gui/TogglePixelWidget.cxx @@ -51,15 +51,18 @@ void TogglePixelWidget::setState(const BoolArray& state) for(int col = 0; col < _cols; col++) { int pos = row * _cols + col; + bool changed = _stateList[pos] != state[pos]; - _changedList[pos] = _stateList[pos] != state[pos]; + if(_changedList[pos] != changed) + { + _changedList[pos] = changed; + setDirty(); + } } } _stateList.clear(); _stateList = state; - - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -113,6 +116,16 @@ int TogglePixelWidget::getIntState() return value; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TogglePixelWidget::setCrossed(bool enable) +{ + if(_crossBits != enable) + { + _crossBits = enable; + setDirty(); + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TogglePixelWidget::drawWidget(bool hilite) { diff --git a/src/debugger/gui/TogglePixelWidget.hxx b/src/debugger/gui/TogglePixelWidget.hxx index fe63cd964..503404c6f 100644 --- a/src/debugger/gui/TogglePixelWidget.hxx +++ b/src/debugger/gui/TogglePixelWidget.hxx @@ -39,7 +39,7 @@ class TogglePixelWidget : public ToggleWidget void setIntState(int value, bool swap = false); int getIntState(); - void setCrossed(bool enable) { _crossBits = enable; } + void setCrossed(bool enable); private: ColorId _pixelColor{kNone}, _backgroundColor{kDlgColor}; diff --git a/src/gui/ColorWidget.cxx b/src/gui/ColorWidget.cxx index 5f31df35d..94ef4bbf4 100644 --- a/src/gui/ColorWidget.cxx +++ b/src/gui/ColorWidget.cxx @@ -37,8 +37,21 @@ ColorWidget::ColorWidget(GuiObject* boss, const GUI::Font& font, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ColorWidget::setColor(ColorId color) { - _color = color; - setDirty(); + if(_color != color) + { + _color = color; + setDirty(); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ColorWidget::setCrossed(bool enable) +{ + if(_crossGrid != enable) + { + _crossGrid = enable; + setDirty(); + } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/ColorWidget.hxx b/src/gui/ColorWidget.hxx index 4d39871fa..0ec099a95 100644 --- a/src/gui/ColorWidget.hxx +++ b/src/gui/ColorWidget.hxx @@ -42,7 +42,7 @@ class ColorWidget : public Widget, public CommandSender void setColor(ColorId color); ColorId getColor() const { return _color; } - void setCrossed(bool enable) { _crossGrid = enable; } + void setCrossed(bool enable); protected: void drawWidget(bool hilite) override; diff --git a/src/gui/EditTextWidget.cxx b/src/gui/EditTextWidget.cxx index 6a0f32877..dde104002 100644 --- a/src/gui/EditTextWidget.cxx +++ b/src/gui/EditTextWidget.cxx @@ -46,7 +46,11 @@ void EditTextWidget::setText(const string& str, bool changed) { EditableWidget::setText(str, changed); _backupString = str; - _changed = changed; + if(_changed != changed) + { + _changed = changed; + setDirty(); + } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/EditableWidget.cxx b/src/gui/EditableWidget.cxx index e015dd899..eaadbaeeb 100644 --- a/src/gui/EditableWidget.cxx +++ b/src/gui/EditableWidget.cxx @@ -43,14 +43,18 @@ EditableWidget::EditableWidget(GuiObject* boss, const GUI::Font& font, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void EditableWidget::setText(const string& str, bool) +void EditableWidget::setText(const string& str, bool changed) { + const string oldEditString = _editString; // Filter input string _editString = ""; for(char c: str) if(_filter(tolower(c))) _editString.push_back(c); + if(oldEditString != _editString) + setDirty(); + myUndoHandler->reset(); myUndoHandler->doo(_editString); @@ -60,8 +64,6 @@ void EditableWidget::setText(const string& str, bool) _editScrollOffset = (_font.getStringWidth(_editString) - (getEditRect().w())); if (_editScrollOffset < 0) _editScrollOffset = 0; - - setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/PopUpWidget.cxx b/src/gui/PopUpWidget.cxx index eb35ed9c3..b4dd27b84 100644 --- a/src/gui/PopUpWidget.cxx +++ b/src/gui/PopUpWidget.cxx @@ -78,7 +78,11 @@ void PopUpWidget::setSelected(const Variant& tag, const Variant& def) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PopUpWidget::setSelectedIndex(int idx, bool changed) { - _changed = changed; + if(_changed != changed) + { + _changed = changed; + setDirty(); + } myMenu->setSelectedIndex(idx); setText(myMenu->getSelectedName()); } @@ -86,6 +90,11 @@ void PopUpWidget::setSelectedIndex(int idx, bool changed) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PopUpWidget::setSelectedMax(bool changed) { + if(_changed != changed) + { + _changed = changed; + setDirty(); + } _changed = changed; myMenu->setSelectedMax(); setText(myMenu->getSelectedName()); diff --git a/src/gui/ScrollBarWidget.cxx b/src/gui/ScrollBarWidget.cxx index 4bacbeece..c9d68cb82 100644 --- a/src/gui/ScrollBarWidget.cxx +++ b/src/gui/ScrollBarWidget.cxx @@ -251,6 +251,9 @@ void ScrollBarWidget::handleMouseLeft() void ScrollBarWidget::recalc() { //cerr << "ScrollBarWidget::recalc()\n"; + int oldSliderHeight = _sliderHeight, + oldSliderPos = _sliderPos; + if(_numEntries > _entriesPerPage) { _sliderHeight = (_h - 2 * _upDownBoxHeight) * _entriesPerPage / _numEntries; @@ -268,7 +271,8 @@ void ScrollBarWidget::recalc() _sliderPos = _upDownBoxHeight; } - setDirty(); + if(oldSliderHeight != _sliderHeight || oldSliderPos != _sliderPos) + setDirty(); // only set dirty when something changed } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/TabWidget.cxx b/src/gui/TabWidget.cxx index 3c7d48258..89967d041 100644 --- a/src/gui/TabWidget.cxx +++ b/src/gui/TabWidget.cxx @@ -110,6 +110,9 @@ void TabWidget::setActiveTab(int tabID, bool show) _tabs[_activeTab].firstWidget = _firstWidget; } + if(_activeTab != tabID) + setDirty(); + _activeTab = tabID; _firstWidget = _tabs[tabID].firstWidget; @@ -138,8 +141,6 @@ void TabWidget::updateActiveTab() if(_tabs[_activeTab].parentWidget) _tabs[_activeTab].parentWidget->loadConfig(); - setDirty(); - // Redraw focused areas _boss->redrawFocus(); // TJ: Does nothing! } diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 61b9296cb..38c46bdbe 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -425,20 +425,19 @@ StaticTextWidget::StaticTextWidget(GuiObject* boss, const GUI::Font& font, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StaticTextWidget::setValue(int value) { - _label = std::to_string(value); - - setDirty(); + setLabel(std::to_string(value)); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StaticTextWidget::setLabel(const string& label) { - _label = label; - - setDirty(); + if(_label != label) + { + _label = label; + setDirty(); + } } - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StaticTextWidget::handleMouseEntered() { @@ -710,12 +709,13 @@ void CheckboxWidget::setFill(FillType type) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void CheckboxWidget::setState(bool state, bool changed) { - if(_state != state) + if(_state != state || _changed != changed) { - _state = state; setDirty(); + + _state = state; + _changed = changed; } - _changed = changed; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 5a650122c76a1ce6bfbeaabb6f18926b4790ddfa Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 28 Nov 2020 16:54:23 +0100 Subject: [PATCH 106/107] made DelayQueueWidget use setDirty removed superfluous code from TiaWidget --- src/debugger/gui/DelayQueueWidget.cxx | 12 ++++++++++-- src/debugger/gui/TiaWidget.cxx | 4 ---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/debugger/gui/DelayQueueWidget.cxx b/src/debugger/gui/DelayQueueWidget.cxx index cca5644c6..d0ad50351 100644 --- a/src/debugger/gui/DelayQueueWidget.cxx +++ b/src/debugger/gui/DelayQueueWidget.cxx @@ -50,7 +50,11 @@ void DelayQueueWidget::loadConfig() { using Common::Base; for (auto&& line : myLines) { if (!delayQueueIterator->isValid()) { - line = ""; + if(line != "") + { + setDirty(); + line = ""; + } continue; } @@ -81,7 +85,11 @@ void DelayQueueWidget::loadConfig() { break; } - line = ss.str(); + if(line != ss.str()) + { + setDirty(); + line = ss.str(); + } delayQueueIterator->next(); } } diff --git a/src/debugger/gui/TiaWidget.cxx b/src/debugger/gui/TiaWidget.cxx index c330bf386..f6bc43e6e 100644 --- a/src/debugger/gui/TiaWidget.cxx +++ b/src/debugger/gui/TiaWidget.cxx @@ -919,14 +919,10 @@ void TiaWidget::handleCommand(CommandSender* sender, int cmd, int data, int id) case kRefP0ID: tia.refP0(myRefP0->getState() ? 1 : 0); - myGRP0->setIntState(myGRP0->getIntState(), !myRefP0->getState()); - myGRP0Old->setIntState(myGRP0Old->getIntState(), !myRefP0->getState()); break; case kRefP1ID: tia.refP1(myRefP1->getState() ? 1 : 0); - myGRP1->setIntState(myGRP1->getIntState(), !myRefP1->getState()); - myGRP1Old->setIntState(myGRP1Old->getIntState(), !myRefP1->getState()); break; case kDelP0ID: From 7091bebd0cbc2037c841ce9916370a2b12b08846 Mon Sep 17 00:00:00 2001 From: Christian Speckner Date: Sat, 28 Nov 2020 20:37:15 +0100 Subject: [PATCH 107/107] Revert "Fix bad use of constexpr." --- don't need that anymore with C++17 This reverts commit 038557ba69ee3720800a5ae3821c64d1c60e2724. --- src/gui/ToolTip.cxx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/gui/ToolTip.cxx b/src/gui/ToolTip.cxx index 12ede067a..a887e0096 100644 --- a/src/gui/ToolTip.cxx +++ b/src/gui/ToolTip.cxx @@ -123,13 +123,12 @@ void ToolTip::release(bool emptyTip) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void ToolTip::show(const string& tip) { - uInt32 maxRows = MAX_ROWS; myTipPos = myMousePos; uInt32 maxWidth = std::min(myWidth - myTextXOfs * 2, uInt32(myFont->getStringWidth(tip))); mySurface->fillRect(1, 1, maxWidth + myTextXOfs * 2 - 2, myHeight - 2, kWidColor); - int lines = std::min(maxRows, + int lines = std::min(MAX_ROWS, uInt32(mySurface->drawString(*myFont, tip, myTextXOfs, myTextYOfs, maxWidth, myHeight - myTextYOfs * 2, kTextColor)));

    Features